Merge remote-tracking branch 'upstream/v2.4.x-release' into improve-shortcut-test

This commit is contained in:
Anton Barboza 2021-10-18 15:25:31 -03:00
commit cac21564a1
68 changed files with 726 additions and 256 deletions

View File

@ -15,7 +15,7 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
def handleCreateBreakoutRoomsCmdMsg(msg: CreateBreakoutRoomsCmdMsg, state: MeetingState2x): MeetingState2x = { def handleCreateBreakoutRoomsCmdMsg(msg: CreateBreakoutRoomsCmdMsg, state: MeetingState2x): MeetingState2x = {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) || liveMeeting.props.meetingProp.isBreakout) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to create breakout room for meeting." val reason = "No permission to create breakout room for meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId,

View File

@ -15,7 +15,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
def handleSetLockSettings(msg: ChangeLockSettingsInMeetingCmdMsg): Unit = { def handleSetLockSettings(msg: ChangeLockSettingsInMeetingCmdMsg): Unit = {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) || liveMeeting.props.meetingProp.isBreakout) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to change lock settings" val reason = "No permission to change lock settings"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -12,7 +12,7 @@ trait ChangeUserRoleCmdMsgHdlr extends RightsManagementTrait {
val outGW: OutMsgRouter val outGW: OutMsgRouter
def handleChangeUserRoleCmdMsg(msg: ChangeUserRoleCmdMsg) { def handleChangeUserRoleCmdMsg(msg: ChangeUserRoleCmdMsg) {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) || liveMeeting.props.meetingProp.isBreakout) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to change user role in meeting." val reason = "No permission to change user role in meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -23,7 +23,7 @@ trait EjectUserFromMeetingCmdMsgHdlr extends RightsManagementTrait {
PermissionCheck.VIEWER_LEVEL, PermissionCheck.VIEWER_LEVEL,
liveMeeting.users2x, liveMeeting.users2x,
msg.header.userId msg.header.userId
)) { ) || liveMeeting.props.meetingProp.isBreakout) {
val reason = "No permission to eject user from meeting." val reason = "No permission to eject user from meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -15,7 +15,7 @@ trait LogoutAndEndMeetingCmdMsgHdlr extends RightsManagementTrait {
val eventBus: InternalEventBus val eventBus: InternalEventBus
def handleLogoutAndEndMeetingCmdMsg(msg: LogoutAndEndMeetingCmdMsg, state: MeetingState2x): Unit = { def handleLogoutAndEndMeetingCmdMsg(msg: LogoutAndEndMeetingCmdMsg, state: MeetingState2x): Unit = {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) || liveMeeting.props.meetingProp.isBreakout) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to end meeting on logout." val reason = "No permission to end meeting on logout."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -13,7 +13,7 @@ trait UpdateWebcamsOnlyForModeratorCmdMsgHdlr extends RightsManagementTrait {
def handleUpdateWebcamsOnlyForModeratorCmdMsg(msg: UpdateWebcamsOnlyForModeratorCmdMsg) { def handleUpdateWebcamsOnlyForModeratorCmdMsg(msg: UpdateWebcamsOnlyForModeratorCmdMsg) {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) || liveMeeting.props.meetingProp.isBreakout) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to change lock settings" val reason = "No permission to change lock settings"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -12,7 +12,7 @@ trait MuteAllExceptPresentersCmdMsgHdlr extends RightsManagementTrait {
val outGW: OutMsgRouter val outGW: OutMsgRouter
def handleMuteAllExceptPresentersCmdMsg(msg: MuteAllExceptPresentersCmdMsg) { def handleMuteAllExceptPresentersCmdMsg(msg: MuteAllExceptPresentersCmdMsg) {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) || liveMeeting.props.meetingProp.isBreakout) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to mute all except presenters." val reason = "No permission to mute all except presenters."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -13,7 +13,7 @@ trait MuteMeetingCmdMsgHdlr extends RightsManagementTrait {
def handleMuteMeetingCmdMsg(msg: MuteMeetingCmdMsg): Unit = { def handleMuteMeetingCmdMsg(msg: MuteMeetingCmdMsg): Unit = {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) || liveMeeting.props.meetingProp.isBreakout) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to mute meeting." val reason = "No permission to mute meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -14,7 +14,7 @@ trait SetGuestPolicyMsgHdlr extends RightsManagementTrait {
val outGW: OutMsgRouter val outGW: OutMsgRouter
def handleSetGuestPolicyMsg(msg: SetGuestPolicyCmdMsg): Unit = { def handleSetGuestPolicyMsg(msg: SetGuestPolicyCmdMsg): Unit = {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) || liveMeeting.props.meetingProp.isBreakout) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to set guest policy in meeting." val reason = "No permission to set guest policy in meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -28,6 +28,14 @@ public class LearningDashboardService {
private static Logger log = LoggerFactory.getLogger(LearningDashboardService.class); private static Logger log = LoggerFactory.getLogger(LearningDashboardService.class);
private static String learningDashboardFilesDir = "/var/bigbluebutton/learning-dashboard"; private static String learningDashboardFilesDir = "/var/bigbluebutton/learning-dashboard";
public File getJsonDataFile(String meetingId, String learningDashboardAccessToken) {
File baseDir = new File(this.getDestinationBaseDirectoryName(meetingId,learningDashboardAccessToken));
if (!baseDir.exists()) baseDir.mkdirs();
File jsonFile = new File(baseDir.getAbsolutePath() + File.separatorChar + "learning_dashboard_data.json");
return jsonFile;
}
public void writeJsonDataFile(String meetingId, String learningDashboardAccessToken, String activityJson) { public void writeJsonDataFile(String meetingId, String learningDashboardAccessToken, String activityJson) {
try { try {
@ -36,10 +44,7 @@ public class LearningDashboardService {
return; return;
} }
File baseDir = new File(this.getDestinationBaseDirectoryName(meetingId,learningDashboardAccessToken)); File jsonFile = this.getJsonDataFile(meetingId,learningDashboardAccessToken);
if (!baseDir.exists()) baseDir.mkdirs();
File jsonFile = new File(baseDir.getAbsolutePath() + File.separatorChar + "learning_dashboard_data.json");
FileOutputStream fileOutput = new FileOutputStream(jsonFile); FileOutputStream fileOutput = new FileOutputStream(jsonFile);
fileOutput.write(activityJson.getBytes()); fileOutput.write(activityJson.getBytes());

View File

@ -17,6 +17,7 @@ class App extends React.Component {
tab: 'overview', tab: 'overview',
meetingId: '', meetingId: '',
learningDashboardAccessToken: '', learningDashboardAccessToken: '',
sessionToken: '',
}; };
} }
@ -30,6 +31,7 @@ class App extends React.Component {
setDashboardParams() { setDashboardParams() {
let learningDashboardAccessToken = ''; let learningDashboardAccessToken = '';
let meetingId = ''; let meetingId = '';
let sessionToken = '';
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries()); const params = Object.fromEntries(urlSearchParams.entries());
@ -38,6 +40,10 @@ class App extends React.Component {
meetingId = params.meeting; meetingId = params.meeting;
} }
if (typeof params.sessionToken !== 'undefined') {
sessionToken = params.sessionToken;
}
if (typeof params.report !== 'undefined') { if (typeof params.report !== 'undefined') {
learningDashboardAccessToken = params.report; learningDashboardAccessToken = params.report;
} else { } else {
@ -56,11 +62,12 @@ class App extends React.Component {
} }
} }
this.setState({ learningDashboardAccessToken, meetingId }, this.fetchActivitiesJson); this.setState({ learningDashboardAccessToken, meetingId, sessionToken },
this.fetchActivitiesJson);
} }
fetchActivitiesJson() { fetchActivitiesJson() {
const { learningDashboardAccessToken, meetingId } = this.state; const { learningDashboardAccessToken, meetingId, sessionToken } = this.state;
if (learningDashboardAccessToken !== '') { if (learningDashboardAccessToken !== '') {
fetch(`${meetingId}/${learningDashboardAccessToken}/learning_dashboard_data.json`) fetch(`${meetingId}/${learningDashboardAccessToken}/learning_dashboard_data.json`)
@ -71,6 +78,24 @@ class App extends React.Component {
}).catch(() => { }).catch(() => {
this.setState({ loading: false }); this.setState({ loading: false });
}); });
} else if (sessionToken !== '') {
const url = new URL('/bigbluebutton/api/learningDashboard', window.location);
fetch(`${url}?sessionToken=${sessionToken}`)
.then((response) => response.json())
.then((json) => {
if (json.response.returncode === 'SUCCESS') {
const jsonData = JSON.parse(json.response.data);
this.setState({ activitiesJson: jsonData, loading: false });
document.title = `Learning Dashboard - ${jsonData.name}`;
} else {
// When meeting is ended the sessionToken stop working, check for new cookies
this.setDashboardParams();
this.setState({ loading: false });
}
})
.catch(() => {
this.setState({ loading: false });
});
} else { } else {
this.setState({ loading: false }); this.setState({ loading: false });
} }
@ -78,7 +103,7 @@ class App extends React.Component {
render() { render() {
const { const {
activitiesJson, tab, learningDashboardAccessToken, loading, activitiesJson, tab, meetingId, learningDashboardAccessToken, sessionToken, loading,
} = this.state; } = this.state;
const { intl } = this.props; const { intl } = this.props;
@ -162,19 +187,59 @@ class App extends React.Component {
} }
function getErrorMessage() { function getErrorMessage() {
if (learningDashboardAccessToken === '') { if (learningDashboardAccessToken === '' && sessionToken === '') {
return intl.formatMessage({ id: 'app.learningDashboard.errors.invalidToken', defaultMessage: 'Invalid session token' }); return intl.formatMessage({ id: 'app.learningDashboard.errors.invalidToken', defaultMessage: 'Invalid session token' });
} }
return intl.formatMessage({ id: 'app.learningDashboard.errors.dataUnavailable', defaultMessage: 'Data is no longer available' });
if (activitiesJson === {} || typeof activitiesJson.name === 'undefined') {
return intl.formatMessage({ id: 'app.learningDashboard.errors.dataUnavailable', defaultMessage: 'Data is no longer available' });
}
return '';
} }
if (loading === false && typeof activitiesJson.name === 'undefined') return <ErrorMessage message={getErrorMessage()} />; function copyPublicLink() {
let url = window.location.href.split('?')[0];
url += `?meeting=${meetingId}&report=${learningDashboardAccessToken}&lang=${intl.locale}`;
navigator.clipboard.writeText(url);
const copiedMessage = intl.formatMessage({ id: 'app.learningDashboard.linkCopied', defaultMessage: 'Link successfully copied' });
alert(copiedMessage);
}
if (loading === false && getErrorMessage() !== '') return <ErrorMessage message={getErrorMessage()} />;
return ( return (
<div className="mx-10"> <div className="mx-10">
<div className="flex items-start justify-between pb-3"> <div className="flex items-start justify-between pb-3">
<h1 className="mt-3 text-2xl font-semibold whitespace-nowrap inline-block"> <h1 className="mt-3 text-2xl font-semibold whitespace-nowrap inline-block">
<FormattedMessage id="app.learningDashboard.dashboardTitle" defaultMessage="Learning Dashboard" /> <FormattedMessage id="app.learningDashboard.dashboardTitle" defaultMessage="Learning Dashboard" />
&nbsp;
{
learningDashboardAccessToken !== ''
? (
<button type="button" onClick={() => { copyPublicLink(); }} className="text-sm font-medium text-blue-500 ease-out" name="teste">
(
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
/>
</svg>
&nbsp;
<FormattedMessage id="app.learningDashboard.shareButton" defaultMessage="Share with others" />
)
</button>
)
: null
}
<br /> <br />
<span className="text-sm font-medium">{activitiesJson.name || ''}</span> <span className="text-sm font-medium">{activitiesJson.name || ''}</span>
</h1> </h1>

View File

@ -60,7 +60,7 @@ class Dashboard extends React.Component {
setRtl() { setRtl() {
const { intlLocale } = this.state; const { intlLocale } = this.state;
if (RTL_LANGUAGES.includes(intlLocale)) { if (RTL_LANGUAGES.includes(intlLocale.substring(0, 2))) {
document.body.parentNode.setAttribute('dir', 'rtl'); document.body.parentNode.setAttribute('dir', 'rtl');
} }
} }

View File

@ -1 +1 @@
git clone --branch v2.5.2 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu git clone --branch v2.6.0-beta.5 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.4-rc-2 BIGBLUEBUTTON_RELEASE=2.4-rc-3

View File

@ -34,15 +34,23 @@ log_history=28
find /var/bigbluebutton/ -maxdepth 1 -type d -name "*-*" -mtime +$history -exec rm -rf '{}' + find /var/bigbluebutton/ -maxdepth 1 -type d -name "*-*" -mtime +$history -exec rm -rf '{}' +
# #
# Delete streams in kurento older than N days # Delete streams from Kurento and mediasoup older than N days
# #
for app in recordings screenshare; do kurento_dir=/var/kurento/
app_dir=/var/kurento/$app mediasoup_dir=/var/mediasoup/
if [[ -d $app_dir ]]; then
find $app_dir -name "*.mkv" -o -name "*.webm" -mtime +$history -delete remove_stale_sfu_raw_files() {
find $app_dir -type d -empty -mtime +$history -exec rmdir '{}' + for app in recordings screenshare; do
fi app_dir="${1}${app}"
done if [[ -d $app_dir ]]; then
find "$app_dir" -name "*.mkv" -o -name "*.webm" -mtime +"$history" -delete
find "$app_dir" -type d -empty -mtime +"$history" -exec rmdir '{}' +
fi
done
}
remove_stale_sfu_raw_files "$kurento_dir"
remove_stale_sfu_raw_files "$mediasoup_dir"
# #
# Delete FreeSWITCH wav/opus recordings older than N days # Delete FreeSWITCH wav/opus recordings older than N days

View File

@ -75,8 +75,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
display: none !important; display: none !important;
} }
textarea::-webkit-input-placeholder, ::-webkit-input-placeholder {
input::-webkit-input-placeholder { color: var(--palette-placeholder-text);
opacity: 1;
}
:-moz-placeholder, /* Firefox 4 to 18 */
::-moz-placeholder { /* Firefox 19+ */
color: var(--palette-placeholder-text); color: var(--palette-placeholder-text);
opacity: 1; opacity: 1;
} }

View File

@ -23,9 +23,18 @@ export default function handleMeetingEnd({ header, body }) {
} }
}; };
Meetings.update({ meetingId }, Meetings.find({ meetingId }).forEach((doc) => {
{ $set: { meetingEnded: true, meetingEndedBy: userId, meetingEndedReason: reason } }, Meetings.update({ meetingId },
(err, num) => { cb(err, num, 'Meeting'); }); {
$set: {
meetingEnded: true,
meetingEndedBy: userId,
meetingEndedReason: reason,
learningDashboardAccessToken: doc.password.learningDashboardAccessToken,
},
},
(err, num) => { cb(err, num, 'Meeting'); });
});
Breakouts.update({ parentMeetingId: meetingId }, Breakouts.update({ parentMeetingId: meetingId },
{ $set: { meetingEnded: true } }, { $set: { meetingEnded: true } },

View File

@ -43,10 +43,8 @@ function meetings(role) {
}, },
}; };
if (User.role === ROLE_MODERATOR) { if (User.role !== ROLE_MODERATOR) {
delete options.fields.password; options.fields.learningDashboardAccessToken = false;
options.fields['password.viewerPass'] = false;
options.fields['password.moderatorPass'] = false;
} }
return Meetings.find(selector, options); return Meetings.find(selector, options);

View File

@ -323,30 +323,6 @@ WebApp.connectHandlers.use('/guestWait', (req, res) => {
res.end(guestWaitHtml); res.end(guestWaitHtml);
}); });
// WASM endpoint to be used to fetch the .wasm models for camera effects
// (blur, virtual background).
// See: /imports/ui/services/virtual-backgrounds/
WebApp.connectHandlers.use('/wasm', (req, res) => {
const pathname = req._parsedUrl.pathname;
let file = "";
let hasError = false;
try {
file = Assets.getBinary(pathname.substr(1, pathname.length-1));
} catch (error) {
hasError = true;
Logger.warn(`Could not find WASM file: ${error}`);
}
res.setHeader('Content-Type', 'application/wasm');
if (hasError) {
res.writeHead(404);
} else {
res.writeHead(200);
}
res.end(file);
});
export const eventEmitter = Redis.emitter; export const eventEmitter = Redis.emitter;
export const redisPubSub = Redis; export const redisPubSub = Redis;

View File

@ -227,7 +227,8 @@ class BreakoutRoom extends PureComponent {
componentDidUpdate(prevProps, prevstate) { componentDidUpdate(prevProps, prevstate) {
if (this.listOfUsers) { if (this.listOfUsers) {
for (let i = 0; i < this.listOfUsers.children.length; i += 1) { for (let i = 0; i < this.listOfUsers.children.length; i += 1) {
const roomList = this.listOfUsers.children[i].getElementsByTagName('div')[0]; const roomWrapperChildren = this.listOfUsers.children[i].getElementsByTagName('div');
const roomList = roomWrapperChildren[roomWrapperChildren.length > 1 ? 1 : 0];
roomList.addEventListener('keydown', this.handleMoveEvent, true); roomList.addEventListener('keydown', this.handleMoveEvent, true);
} }
} }

View File

@ -192,7 +192,7 @@ class BreakoutJoinConfirmation extends Component {
)) ))
} }
</select> </select>
{ waiting ? <span>{intl.formatMessage(intlMessages.generatingURL)}</span> : null} { waiting ? <span data-test="labelGeneratingURL">{intl.formatMessage(intlMessages.generatingURL)}</span> : null}
</div> </div>
); );
} }

View File

@ -536,6 +536,7 @@ class BreakoutRoom extends PureComponent {
size="lg" size="lg"
label={intl.formatMessage(intlMessages.endAllBreakouts)} label={intl.formatMessage(intlMessages.endAllBreakouts)}
className={styles.endButton} className={styles.endButton}
data-test="endBreakoutRoomsButton"
onClick={() => { onClick={() => {
this.closePanel(); this.closePanel();
endAllBreakouts(); endAllBreakouts();

View File

@ -20,23 +20,45 @@ const isModerator = () => {
return false; return false;
}; };
const getLearningDashboardAccessToken = () => (( const isLearningDashboardEnabled = () => (((
Meetings.findOne( Meetings.findOne(
{ meetingId: Auth.meetingID }, { meetingId: Auth.meetingID },
{ {
fields: { 'password.learningDashboardAccessToken': 1 }, fields: { 'meetingProp.learningDashboardEnabled': 1 },
}, },
) || {}).password || {}).learningDashboardAccessToken || null; ) || {}).meetingProp || {}).learningDashboardEnabled || false);
const getLearningDashboardAccessToken = () => ((
Meetings.findOne(
{ meetingId: Auth.meetingID, learningDashboardAccessToken: { $exists: true } },
{
fields: { learningDashboardAccessToken: 1 },
},
) || {}).learningDashboardAccessToken || null);
const setLearningDashboardCookie = () => {
const learningDashboardAccessToken = getLearningDashboardAccessToken();
if (learningDashboardAccessToken !== null) {
const cookieExpiresDate = new Date();
cookieExpiresDate.setTime(cookieExpiresDate.getTime() + (3600000 * 24 * 30)); // keep cookie 30d
document.cookie = `learningDashboardAccessToken-${Auth.meetingID}=${getLearningDashboardAccessToken()}; expires=${cookieExpiresDate.toGMTString()}; path=/`;
return true;
}
return false;
};
const openLearningDashboardUrl = (lang) => { const openLearningDashboardUrl = (lang) => {
const cookieExpiresDate = new Date(); if (getLearningDashboardAccessToken() && setLearningDashboardCookie()) {
cookieExpiresDate.setTime(cookieExpiresDate.getTime() + (3600000 * 24 * 30)); // keep cookie 30d window.open(`/learning-dashboard/?meeting=${Auth.meetingID}&lang=${lang}`, '_blank');
document.cookie = `learningDashboardAccessToken-${Auth.meetingID}=${getLearningDashboardAccessToken()}; expires=${cookieExpiresDate.toGMTString()}; path=/`; } else {
window.open(`/learning-dashboard/?meeting=${Auth.meetingID}&lang=${lang}`, '_blank'); window.open(`/learning-dashboard/?meeting=${Auth.meetingID}&sessionToken=${Auth.sessionToken}&lang=${lang}`, '_blank');
}
}; };
export default { export default {
isModerator, isModerator,
isLearningDashboardEnabled,
getLearningDashboardAccessToken, getLearningDashboardAccessToken,
setLearningDashboardCookie,
openLearningDashboardUrl, openLearningDashboardUrl,
}; };

View File

@ -266,7 +266,9 @@ class MeetingEnded extends PureComponent {
<div> <div>
{ {
LearningDashboardService.isModerator() LearningDashboardService.isModerator()
&& LearningDashboardService.getLearningDashboardAccessToken() != null && LearningDashboardService.isLearningDashboardEnabled() === true
// Always set cookie in case Dashboard is already opened
&& LearningDashboardService.setLearningDashboardCookie() === true
? ( ? (
<div className={styles.text}> <div className={styles.text}>
<Button <Button

View File

@ -4,6 +4,7 @@ import _ from 'lodash';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/button/component'; import Button from '/imports/ui/components/button/component';
import { styles } from './styles'; import { styles } from './styles';
import Service from './service';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
wasTalking: { wasTalking: {
@ -22,6 +23,14 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.muteLabel', id: 'app.actionsBar.muteLabel',
description: 'indicator mute label for moderators', description: 'indicator mute label for moderators',
}, },
moreThanMaxIndicatorsTalking: {
id: 'app.talkingIndicator.moreThanMaxIndicatorsTalking',
description: 'indicator label for all users who is talking but not visible',
},
moreThanMaxIndicatorsWereTalking: {
id: 'app.talkingIndicator.moreThanMaxIndicatorsWereTalking',
description: 'indicator label for all users who is not talking but not visible',
},
}); });
class TalkingIndicator extends PureComponent { class TalkingIndicator extends PureComponent {
@ -39,6 +48,7 @@ class TalkingIndicator extends PureComponent {
amIModerator, amIModerator,
sidebarNavigationIsOpen, sidebarNavigationIsOpen,
sidebarContentIsOpen, sidebarContentIsOpen,
moreThanMaxIndicators,
} = this.props; } = this.props;
if (!talkers) return null; if (!talkers) return null;
@ -76,8 +86,7 @@ class TalkingIndicator extends PureComponent {
label={callerName} label={callerName}
tooltipLabel={!muted && amIModerator tooltipLabel={!muted && amIModerator
? `${intl.formatMessage(intlMessages.muteLabel)} ${callerName}` ? `${intl.formatMessage(intlMessages.muteLabel)} ${callerName}`
: null : null}
}
data-test={talking ? 'isTalking' : 'wasTalking'} data-test={talking ? 'isTalking' : 'wasTalking'}
aria-label={ariaLabel} aria-label={ariaLabel}
aria-describedby={talking ? 'description' : null} aria-describedby={talking ? 'description' : null}
@ -93,16 +102,55 @@ class TalkingIndicator extends PureComponent {
<div id="description" className={styles.hidden}> <div id="description" className={styles.hidden}>
{`${intl.formatMessage(intlMessages.ariaMuteDesc)}`} {`${intl.formatMessage(intlMessages.ariaMuteDesc)}`}
</div> </div>
) : null ) : null}
}
</Button> </Button>
); );
}); });
const maxIndicator = () => {
if (!moreThanMaxIndicators) return null;
const nobodyTalking = Service.nobodyTalking(talkers);
const style = {
[styles.talker]: true,
[styles.spoke]: nobodyTalking,
// [styles.muted]: false,
[styles.mobileHide]: sidebarNavigationIsOpen
&& sidebarContentIsOpen,
};
const { moreThanMaxIndicatorsTalking, moreThanMaxIndicatorsWereTalking } = intlMessages;
const ariaLabel = intl.formatMessage(nobodyTalking
? moreThanMaxIndicatorsWereTalking : moreThanMaxIndicatorsTalking, {
0: Object.keys(talkers).length,
});
return (
<Button
key={_.uniqueId('_has__More_')}
className={cx(style)}
onClick={() => {}} // maybe add a dropdown to show the rest of the users
label="..."
tooltipLabel={ariaLabel}
aria-label={ariaLabel}
color="primary"
size="sm"
style={{
backgroundColor: '#4a148c',
border: 'solid 2px #4a148c',
cursor: 'default',
}}
/>
);
};
return ( return (
<div className={styles.isTalkingWrapper}> <div className={styles.isTalkingWrapper}>
<div className={styles.speaking}> <div className={styles.speaking}>
{talkingUserElements} {talkingUserElements}
{maxIndicator()}
</div> </div>
</div> </div>
); );

View File

@ -6,12 +6,12 @@ import { debounce } from 'lodash';
import TalkingIndicator from './component'; import TalkingIndicator from './component';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
import { meetingIsBreakout } from '/imports/ui/components/app/service'; import { meetingIsBreakout } from '/imports/ui/components/app/service';
import Service from './service';
import LayoutContext from '../../layout/context'; import LayoutContext from '../../layout/context';
const APP_CONFIG = Meteor.settings.public.app; const APP_CONFIG = Meteor.settings.public.app;
const { enableTalkingIndicator } = APP_CONFIG; const { enableTalkingIndicator } = APP_CONFIG;
const TALKING_INDICATOR_MUTE_INTERVAL = 500; const TALKING_INDICATOR_MUTE_INTERVAL = 500;
const TALKING_INDICATORS_MAX = 8;
const TalkingIndicatorContainer = (props) => { const TalkingIndicatorContainer = (props) => {
if (!enableTalkingIndicator) return null; if (!enableTalkingIndicator) return null;
@ -50,10 +50,18 @@ export default withTracker(() => {
muted: 1, muted: 1,
intId: 1, intId: 1,
}, },
}).fetch().sort(Service.sortVoiceUsers); sort: {
startTime: 1,
},
limit: TALKING_INDICATORS_MAX + 1,
}).fetch();
if (usersTalking) { if (usersTalking) {
for (let i = 0; i < usersTalking.length; i += 1) { const maxNumberVoiceUsersNotification = usersTalking.length < TALKING_INDICATORS_MAX
? usersTalking.length
: TALKING_INDICATORS_MAX;
for (let i = 0; i < maxNumberVoiceUsersNotification; i += 1) {
const { const {
callerName, talking, color, voiceUserId, muted, intId, callerName, talking, color, voiceUserId, muted, intId,
} = usersTalking[i]; } = usersTalking[i];
@ -82,5 +90,6 @@ export default withTracker(() => {
talkers, talkers,
muteUser, muteUser,
isBreakoutRoom: meetingIsBreakout(), isBreakoutRoom: meetingIsBreakout(),
moreThanMaxIndicators: usersTalking.length > TALKING_INDICATORS_MAX,
}; };
})(TalkingIndicatorContainer); })(TalkingIndicatorContainer);

View File

@ -1,14 +1,8 @@
const sortByStartTime = (a, b) => { const nobodyTalking = (talkers) => {
if (a.startTime < b.startTime) return -1; const values = Object.values(talkers);
if (a.startTime > b.startTime) return 1; return values.every(({ talking }) => talking === false);
return 0;
};
const sortVoiceUsers = (a, b) => {
const sort = sortByStartTime(a, b);
return sort;
}; };
export default { export default {
sortVoiceUsers, nobodyTalking,
}; };

View File

@ -36,6 +36,8 @@
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
max-height: var(--talker-padding-xl); max-height: var(--talker-padding-xl);
scrollbar-width: 0; // firefox
scrollbar-color: transparent;
} }
.speaking::-webkit-scrollbar { .speaking::-webkit-scrollbar {

View File

@ -326,7 +326,7 @@ class ApplicationMenu extends BaseMenu {
return ( return (
<> <>
<div className={styles.row}> <div className={styles.row}>
<div className={styles.col} aria-hidden="true"> <div className={styles.col}>
<div className={styles.formElement}> <div className={styles.formElement}>
<label htmlFor="layoutList" className={styles.label}> <label htmlFor="layoutList" className={styles.label}>
{intl.formatMessage(intlMessages.layoutOptionLabel)} {intl.formatMessage(intlMessages.layoutOptionLabel)}
@ -412,7 +412,7 @@ class ApplicationMenu extends BaseMenu {
{this.renderPaginationToggle()} {this.renderPaginationToggle()}
<div className={styles.row}> <div className={styles.row}>
<div className={styles.col} aria-hidden="true"> <div className={styles.col}>
<div className={styles.formElement}> <div className={styles.formElement}>
<label <label
className={styles.label} className={styles.label}

View File

@ -66,6 +66,8 @@ class UserParticipants extends Component {
} }
componentDidMount() { componentDidMount() {
document.getElementById('user-list-virtualized-scroll')?.getElementsByTagName('div')[0]?.firstElementChild?.setAttribute('aria-label', 'Users list');
const { compact } = this.props; const { compact } = this.props;
if (!compact) { if (!compact) {
this.refScrollContainer.addEventListener( this.refScrollContainer.addEventListener(
@ -220,6 +222,8 @@ class UserParticipants extends Component {
} }
<div <div
id={'user-list-virtualized-scroll'} id={'user-list-virtualized-scroll'}
aria-label="Users list"
role="region"
className={styles.virtulizedScrollableList} className={styles.virtulizedScrollableList}
tabIndex={0} tabIndex={0}
ref={(ref) => { ref={(ref) => {

View File

@ -214,7 +214,7 @@ class UserOptions extends PureComponent {
hasBreakoutRoom, hasBreakoutRoom,
isBreakoutEnabled, isBreakoutEnabled,
getUsersNotAssigned, getUsersNotAssigned,
learningDashboardAccessToken, learningDashboardEnabled,
openLearningDashboardUrl, openLearningDashboardUrl,
amIModerator, amIModerator,
users, users,
@ -326,7 +326,7 @@ class UserOptions extends PureComponent {
}); });
} }
if (amIModerator) { if (amIModerator) {
if (learningDashboardAccessToken != null) { if (learningDashboardEnabled === true) {
this.menuItems.push({ this.menuItems.push({
icon: 'multi_whiteboard', icon: 'multi_whiteboard',
iconRight: 'popout_window', iconRight: 'popout_window',
@ -336,7 +336,7 @@ class UserOptions extends PureComponent {
onClick: () => { openLearningDashboardUrl(locale); }, onClick: () => { openLearningDashboardUrl(locale); },
dividerTop: true, dividerTop: true,
}); });
} }
} }
} }

View File

@ -92,7 +92,7 @@ const UserOptionsContainer = withTracker((props) => {
guestPolicy: WaitingUsersService.getGuestPolicy(), guestPolicy: WaitingUsersService.getGuestPolicy(),
isMeteorConnected: Meteor.status().connected, isMeteorConnected: Meteor.status().connected,
meetingName: getMeetingName(), meetingName: getMeetingName(),
learningDashboardAccessToken: LearningDashboardService.getLearningDashboardAccessToken(), learningDashboardEnabled: LearningDashboardService.isLearningDashboardEnabled(),
openLearningDashboardUrl: LearningDashboardService.openLearningDashboardUrl, openLearningDashboardUrl: LearningDashboardService.openLearningDashboardUrl,
dynamicGuestPolicy, dynamicGuestPolicy,
}; };

View File

@ -98,8 +98,8 @@
.content { .content {
display: flex; display: flex;
height: 14rem; min-height: 14rem;
max-height: 40vh; max-height: 50vh;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

@ -2,8 +2,7 @@ import React, { useContext } from 'react';
import { withModalMounter } from '/imports/ui/components/modal/service'; import { withModalMounter } from '/imports/ui/components/modal/service';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import Settings from '/imports/ui/services/settings'; import MediaService, { getSwapLayout } from '/imports/ui/components/media/service';
import MediaService, { getSwapLayout, } from '/imports/ui/components/media/service';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import breakoutService from '/imports/ui/components/breakout-room/service'; import breakoutService from '/imports/ui/components/breakout-room/service';
import VideoService from '/imports/ui/components/video-provider/service'; import VideoService from '/imports/ui/components/video-provider/service';
@ -16,11 +15,12 @@ const WebcamContainer = ({
audioModalIsOpen, audioModalIsOpen,
swapLayout, swapLayout,
usersVideo, usersVideo,
disableVideo,
}) => { }) => {
const layoutContext = useContext(LayoutContext); const layoutContext = useContext(LayoutContext);
const { layoutContextState, layoutContextDispatch } = layoutContext; const { layoutContextState, layoutContextDispatch } = layoutContext;
const { fullscreen, output, input, isRTL } = layoutContextState; const {
fullscreen, output, input, isRTL,
} = layoutContextState;
const { cameraDock, presentation } = output; const { cameraDock, presentation } = output;
const { cameraDock: cameraDockInput } = input; const { cameraDock: cameraDockInput } = input;
const { cameraOptimalGridSize } = cameraDockInput; const { cameraOptimalGridSize } = cameraDockInput;
@ -30,8 +30,7 @@ const WebcamContainer = ({
const { users } = usingUsersContext; const { users } = usingUsersContext;
const currentUser = users[Auth.meetingID][Auth.userID]; const currentUser = users[Auth.meetingID][Auth.userID];
return !disableVideo return !audioModalIsOpen
&& !audioModalIsOpen
&& usersVideo.length > 0 && usersVideo.length > 0
? ( ? (
<WebcamComponent <WebcamComponent
@ -54,8 +53,6 @@ const WebcamContainer = ({
let userWasInBreakout = false; let userWasInBreakout = false;
export default withModalMounter(withTracker(() => { export default withModalMounter(withTracker(() => {
const { dataSaving } = Settings;
const { viewParticipantsWebcams } = dataSaving;
const { current_presentation: hasPresentation } = MediaService.getPresentationInfo(); const { current_presentation: hasPresentation } = MediaService.getPresentationInfo();
const data = { const data = {
audioModalIsOpen: Session.get('audioModalIsOpen'), audioModalIsOpen: Session.get('audioModalIsOpen'),
@ -90,7 +87,6 @@ export default withModalMounter(withTracker(() => {
const { streams: usersVideo } = VideoService.getVideoStreams(); const { streams: usersVideo } = VideoService.getVideoStreams();
data.usersVideo = usersVideo; data.usersVideo = usersVideo;
data.swapLayout = getSwapLayout() || !hasPresentation; data.swapLayout = getSwapLayout() || !hasPresentation;
data.disableVideo = !viewParticipantsWebcams;
if (data.swapLayout) { if (data.swapLayout) {
data.floatingOverlay = true; data.floatingOverlay = true;

View File

@ -335,7 +335,7 @@
<body> <body>
<div id="content"> <div id="content">
<h1 id="heading">Guest Lobby</h1> <h1 id="heading">BigBlueButton - Guest Lobby</h1>
<div class="spinner"> <div class="spinner">
<div class="bounce1"></div> <div class="bounce1"></div>
<div class="bounce2"></div> <div class="bounce2"></div>

View File

@ -411,6 +411,8 @@
"app.switch.offLabel": "OFF", "app.switch.offLabel": "OFF",
"app.talkingIndicator.ariaMuteDesc" : "Select to mute user", "app.talkingIndicator.ariaMuteDesc" : "Select to mute user",
"app.talkingIndicator.isTalking" : "{0} is talking", "app.talkingIndicator.isTalking" : "{0} is talking",
"app.talkingIndicator.moreThanMaxIndicatorsTalking" : "{0}+ are talking",
"app.talkingIndicator.moreThanMaxIndicatorsWereTalking" : "{0}+ were talking",
"app.talkingIndicator.wasTalking" : "{0} stopped talking", "app.talkingIndicator.wasTalking" : "{0} stopped talking",
"app.actionsBar.actionsDropdown.actionsLabel": "Actions", "app.actionsBar.actionsDropdown.actionsLabel": "Actions",
"app.actionsBar.actionsDropdown.presentationLabel": "Manage presentations", "app.actionsBar.actionsDropdown.presentationLabel": "Manage presentations",
@ -910,6 +912,8 @@
"playback.player.video.wrapper.aria": "Video area", "playback.player.video.wrapper.aria": "Video area",
"app.learningDashboard.dashboardTitle": "Learning Dashboard", "app.learningDashboard.dashboardTitle": "Learning Dashboard",
"app.learningDashboard.user": "User", "app.learningDashboard.user": "User",
"app.learningDashboard.shareButton": "Share with others",
"app.learningDashboard.shareLinkCopied": "Link successfully copied",
"app.learningDashboard.indicators.meetingStatusEnded": "Ended", "app.learningDashboard.indicators.meetingStatusEnded": "Ended",
"app.learningDashboard.indicators.meetingStatusActive": "Active", "app.learningDashboard.indicators.meetingStatusActive": "Active",
"app.learningDashboard.indicators.usersOnline": "Active Users", "app.learningDashboard.indicators.usersOnline": "Active Users",

View File

@ -1,6 +1,5 @@
const Page = require('../core/page'); const Page = require('../core/page');
const e = require('../core/elements'); const e = require('../core/elements');
const { checkElement } = require('../core/util');
const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants'); const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
class Create { class Create {
@ -62,7 +61,7 @@ class Create {
// Check if Breakoutrooms have been created // Check if Breakoutrooms have been created
async testCreatedBreakout(testName) { async testCreatedBreakout(testName) {
try { try {
const resp = await this.modPage1.page.evaluate(checkElement, e.breakoutRoomsItem); const resp = await this.modPage1.hasElement(e.breakoutRoomsItem);
if (resp === true) { if (resp === true) {
await this.modPage1.screenshot(`${testName}`, `05-page01-success-${testName}`); await this.modPage1.screenshot(`${testName}`, `05-page01-success-${testName}`);
@ -88,8 +87,7 @@ class Create {
await this.modPage2.waitAndClick(e.chatButton); await this.modPage2.waitAndClick(e.chatButton);
await this.modPage2.waitAndClick(e.breakoutRoomsItem); await this.modPage2.waitAndClick(e.breakoutRoomsItem);
await this.modPage2.waitAndClick(e.generateRoom1); await this.modPage2.waitAndClick(e.askJoinRoom1);
await this.modPage2.waitAndClick(e.joinGeneratedRoom1);
await this.modPage2.waitForSelector(e.alreadyConnected, ELEMENT_WAIT_LONGER_TIME); await this.modPage2.waitForSelector(e.alreadyConnected, ELEMENT_WAIT_LONGER_TIME);
const breakoutModPage2 = await this.modPage2.getLastTargetPage(); const breakoutModPage2 = await this.modPage2.getLastTargetPage();
@ -107,8 +105,7 @@ class Create {
} else if (testName === 'joinBreakoutroomsWithVideo') { } else if (testName === 'joinBreakoutroomsWithVideo') {
await this.modPage2.init(true, true, testName, 'Moderator2', this.modPage1.meetingId); await this.modPage2.init(true, true, testName, 'Moderator2', this.modPage1.meetingId);
await this.modPage2.waitAndClick(e.breakoutRoomsButton); await this.modPage2.waitAndClick(e.breakoutRoomsButton);
await this.modPage2.waitAndClick(e.generateRoom1); await this.modPage2.waitAndClick(e.askJoinRoom1);
await this.modPage2.waitAndClick(e.joinGeneratedRoom1);
await this.modPage2.waitForSelector(e.alreadyConnected); await this.modPage2.waitForSelector(e.alreadyConnected);
const breakoutModPage2 = await this.modPage2.getLastTargetPage(); const breakoutModPage2 = await this.modPage2.getLastTargetPage();
@ -116,18 +113,15 @@ class Create {
await breakoutModPage2.bringToFront(); await breakoutModPage2.bringToFront();
await breakoutModPage2.closeAudioModal(); await breakoutModPage2.closeAudioModal();
await breakoutModPage2.waitAndClick(e.joinVideo);
const parsedSettings = await this.modPage2.getSettingsYaml(); const parsedSettings = await this.modPage2.getSettingsYaml();
const videoPreviewTimeout = parseInt(parsedSettings.public.kurento.gUMTimeout); const videoPreviewTimeout = parseInt(parsedSettings.public.kurento.gUMTimeout);
await breakoutModPage2.waitAndClick(e.videoPreview, videoPreviewTimeout); await breakoutModPage2.shareWebcam(true, videoPreviewTimeout);
await breakoutModPage2.waitAndClick(e.startSharingWebcam);
await breakoutModPage2.screenshot(testName, '00-breakout-page03-user-joined-with-webcam-before-check'); await breakoutModPage2.screenshot(testName, '00-breakout-page03-user-joined-with-webcam-before-check');
} else if (testName === 'joinBreakoutroomsAndShareScreen') { } else if (testName === 'joinBreakoutroomsAndShareScreen') {
await this.modPage2.init(true, true, testName, 'Moderator2', this.modPage1.meetingId); await this.modPage2.init(true, true, testName, 'Moderator2', this.modPage1.meetingId);
await this.modPage2.waitAndClick(e.breakoutRoomsButton); await this.modPage2.waitAndClick(e.breakoutRoomsButton);
await this.modPage2.waitAndClick(e.generateRoom1); await this.modPage2.waitAndClick(e.askJoinRoom1);
await this.modPage2.waitAndClick(e.joinGeneratedRoom1);
await this.modPage2.waitForSelector(e.alreadyConnected); await this.modPage2.waitForSelector(e.alreadyConnected);
const breakoutModPage2 = await this.modPage2.getLastTargetPage(); const breakoutModPage2 = await this.modPage2.getLastTargetPage();

View File

@ -10,6 +10,7 @@ exports.CLIENT_RECONNECTION_TIMEOUT = 120000;
// STRESS TESTS VARS // STRESS TESTS VARS
exports.JOIN_AS_MODERATOR_TEST_ROUNDS = 100; exports.JOIN_AS_MODERATOR_TEST_ROUNDS = 100;
exports.MAX_JOIN_AS_MODERATOR_FAIL_RATE = 0.05; exports.MAX_JOIN_AS_MODERATOR_FAIL_RATE = 0.05;
exports.BREAKOUT_ROOM_INVITATION_TEST_ROUNDS = 20;
// MEDIA CONNECTION TIMEOUTS // MEDIA CONNECTION TIMEOUTS
exports.VIDEO_LOADING_WAIT_TIME = 15000; exports.VIDEO_LOADING_WAIT_TIME = 15000;

View File

@ -49,9 +49,11 @@ exports.alreadyConnected = 'span[class^="alreadyConnected--"]';
exports.breakoutJoin = '[data-test="breakoutJoin"]'; exports.breakoutJoin = '[data-test="breakoutJoin"]';
exports.userJoined = 'div[aria-label^="Moderator3"]'; exports.userJoined = 'div[aria-label^="Moderator3"]';
exports.breakoutRoomsButton = 'div[aria-label="Breakout Rooms"]'; exports.breakoutRoomsButton = 'div[aria-label="Breakout Rooms"]';
exports.generateRoom1 = 'button[aria-label="Generate URL Room 1"]'; exports.askJoinRoom1 = 'button[aria-label="Ask to join Room 1"]';
exports.joinGeneratedRoom1 = 'button[aria-label="Generated Room 1"]';
exports.joinRoom1 = 'button[aria-label="Join room Room 1"]'; exports.joinRoom1 = 'button[aria-label="Join room Room 1"]';
exports.allowChoiceRoom = 'input[id="freeJoinCheckbox"]';
exports.labelGeneratingURL = 'span[data-test="labelGeneratingURL"]';
exports.endBreakoutRoomsButton = 'button[data-test="endBreakoutRoomsButton"]';
// Chat // Chat
exports.chatButton = 'div[data-test="chatButton"]'; exports.chatButton = 'div[data-test="chatButton"]';
@ -204,6 +206,8 @@ exports.chatPanel = 'section[data-test="chatPanel"]';
exports.userListPanel = 'div[data-test="userListPanel"]'; exports.userListPanel = 'div[data-test="userListPanel"]';
exports.multiWhiteboardTool = 'span[data-test="multiWhiteboardTool"]' exports.multiWhiteboardTool = 'span[data-test="multiWhiteboardTool"]'
exports.connectionStatusBtn = 'button[data-test="connectionStatusButton"]'; exports.connectionStatusBtn = 'button[data-test="connectionStatusButton"]';
exports.connectionDataContainer = '[class^=networkDataContainer--]';
exports.connectionNetwordData = '[class^=networkData--]';
// Webcam // Webcam
exports.joinVideo = 'button[data-test="joinVideo"]'; exports.joinVideo = 'button[data-test="joinVideo"]';

View File

@ -8,7 +8,7 @@ const path = require('path');
const PuppeteerVideoRecorder = require('puppeteer-video-recorder'); const PuppeteerVideoRecorder = require('puppeteer-video-recorder');
const helper = require('./helper'); const helper = require('./helper');
const params = require('./params'); const params = require('./params');
const { ELEMENT_WAIT_TIME } = require('./constants'); const { ELEMENT_WAIT_TIME, VIDEO_LOADING_WAIT_TIME } = require('./constants');
const { getElementLength } = require('./util'); const { getElementLength } = require('./util');
const e = require('./elements'); const e = require('./elements');
const { NETWORK_PRESETS } = require('./profiles'); const { NETWORK_PRESETS } = require('./profiles');
@ -104,6 +104,17 @@ class Page {
await this.waitForSelector(e.isTalking); await this.waitForSelector(e.isTalking);
} }
async shareWebcam(shouldConfirmSharing, videoPreviewTimeout = ELEMENT_WAIT_TIME) {
await this.waitAndClick(e.joinVideo);
if (shouldConfirmSharing) {
await this.waitForSelector(e.videoPreview, videoPreviewTimeout);
await this.waitAndClick(e.startSharingWebcam);
}
await this.waitForSelector(e.webcamConnecting);
await this.waitForSelector(e.webcamVideo, VIDEO_LOADING_WAIT_TIME);
await this.waitForSelector(e.leaveVideo, VIDEO_LOADING_WAIT_TIME);
}
// Joining audio with microphone // Joining audio with microphone
async joinMicrophoneWithoutEchoTest() { async joinMicrophoneWithoutEchoTest() {
await this.waitAndClick(e.joinAudio); await this.waitAndClick(e.joinAudio);
@ -214,16 +225,6 @@ class Page {
} }
} }
async isNotVisible(element, timeout = ELEMENT_WAIT_TIME) {
try {
await this.hasElement(element, false, timeout);
return true;
} catch (err) {
await this.logger(err);
return false;
}
}
// async emulateMobile(userAgent) { // async emulateMobile(userAgent) {
// await this.page.setUserAgent(userAgent); // await this.page.setUserAgent(userAgent);
// } // }
@ -243,7 +244,7 @@ class Page {
} }
} }
async hasElement(element, visible = false, timeout = ELEMENT_WAIT_TIME) { async hasElement(element, visible = true, timeout = ELEMENT_WAIT_TIME) {
try { try {
await this.page.waitForSelector(element, { visible, timeout }); await this.page.waitForSelector(element, { visible, timeout });
return true; return true;

View File

@ -3,9 +3,8 @@ exports.listenOnlyMode = 'userdata-bbb_listen_only_mode=false';
exports.forceListenOnly = 'userdata-bbb_force_listen_only=true'; exports.forceListenOnly = 'userdata-bbb_force_listen_only=true';
exports.skipCheck = 'userdata-bbb_skip_check_audio=true'; exports.skipCheck = 'userdata-bbb_skip_check_audio=true';
exports.skipCheckOnFirstJoin = 'userdata-bbb_skip_check_audio_on_first_join=true'; exports.skipCheckOnFirstJoin = 'userdata-bbb_skip_check_audio_on_first_join=true';
const docTitle = 'puppeteer'; exports.docTitle = 'puppeteer';
exports.docTitle = docTitle; exports.clientTitle = `userdata-bbb_client_title=${this.docTitle}`;
exports.clientTitle = `userdata-bbb_client_title=${docTitle}`;
exports.askForFeedbackOnLogout = 'userdata-bbb_ask_for_feedback_on_logout=true'; exports.askForFeedbackOnLogout = 'userdata-bbb_ask_for_feedback_on_logout=true';
exports.displayBrandingArea = 'userdata-bbb_display_branding_area=true'; exports.displayBrandingArea = 'userdata-bbb_display_branding_area=true';
exports.logo = 'logo=https://bigbluebutton.org/wp-content/themes/bigbluebutton/library/images/bigbluebutton-logo.png'; exports.logo = 'logo=https://bigbluebutton.org/wp-content/themes/bigbluebutton/library/images/bigbluebutton-logo.png';

View File

@ -3,7 +3,7 @@ const e = require('../core/elements');
const c = require('./constants'); const c = require('./constants');
const util = require('./util'); const util = require('./util');
const { VIDEO_LOADING_WAIT_TIME, ELEMENT_WAIT_LONGER_TIME } = require('../core/constants'); // core constants (Timeouts vars imported) const { VIDEO_LOADING_WAIT_TIME, ELEMENT_WAIT_LONGER_TIME } = require('../core/constants'); // core constants (Timeouts vars imported)
const { checkElementLengthEqualTo, checkElementLengthDifferentTo } = require('../core/util'); const { checkElementLengthEqualTo, checkElement } = require('../core/util');
class CustomParameters { class CustomParameters {
constructor() { constructor() {
@ -25,7 +25,7 @@ class CustomParameters {
await this.page1.startRecording(testName); await this.page1.startRecording(testName);
await this.page1.screenshot(`${testName}`, `01-${testName}`); await this.page1.screenshot(`${testName}`, `01-${testName}`);
await this.page1.waitForSelector(e.chatMessages); await this.page1.waitForSelector(e.chatMessages);
const resp = await this.page1.page.evaluate(checkElementLengthEqualTo, e.audioModal, 0); const resp = await this.page1.wasRemoved(e.audioModal);
if (!resp) { if (!resp) {
await this.page1.screenshot(`${testName}`, `02-fail-${testName}`); await this.page1.screenshot(`${testName}`, `02-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
@ -96,7 +96,7 @@ class CustomParameters {
await this.page1.screenshot(`${testName}`, `02-${testName}`); await this.page1.screenshot(`${testName}`, `02-${testName}`);
await this.page1.waitForElementHandleToBeRemoved(e.connectingStatus, ELEMENT_WAIT_LONGER_TIME); await this.page1.waitForElementHandleToBeRemoved(e.connectingStatus, ELEMENT_WAIT_LONGER_TIME);
await this.page1.screenshot(`${testName}`, `03-${testName}`); await this.page1.screenshot(`${testName}`, `03-${testName}`);
const resp = await this.page1.page.evaluate(checkElementLengthEqualTo, e.echoYesButton, 0); const resp = await this.page1.wasRemoved(e.echoYesButton);
if (!resp) { if (!resp) {
await this.page1.screenshot(`${testName}`, `04-fail-${testName}`); await this.page1.screenshot(`${testName}`, `04-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
@ -118,13 +118,13 @@ class CustomParameters {
await this.page1.startRecording(testName); await this.page1.startRecording(testName);
await this.page1.screenshot(`${testName}`, `01-${testName}`); await this.page1.screenshot(`${testName}`, `01-${testName}`);
await this.page1.waitAndClick(e.microphoneButton); await this.page1.waitAndClick(e.microphoneButton);
const firstCheck = await this.page1.page.evaluate(checkElementLengthDifferentTo, e.connecting, 0); const firstCheck = await this.page1.hasElement(e.connecting);
await this.page1.screenshot(`${testName}`, `02-${testName}`); await this.page1.screenshot(`${testName}`, `02-${testName}`);
await this.page1.leaveAudio(); await this.page1.leaveAudio();
await this.page1.screenshot(`${testName}`, `03-${testName}`); await this.page1.screenshot(`${testName}`, `03-${testName}`);
await this.page1.waitAndClick(e.joinAudio); await this.page1.waitAndClick(e.joinAudio);
await this.page1.waitAndClick(e.microphoneButton); await this.page1.waitAndClick(e.microphoneButton);
const secondCheck = await this.page1.page.evaluate(checkElementLengthDifferentTo, e.connectingToEchoTest, 0); const secondCheck = await this.page1.hasElement(e.connectingToEchoTest);
if (firstCheck !== secondCheck) { if (firstCheck !== secondCheck) {
await this.page1.screenshot(`${testName}`, `04-fail-${testName}`); await this.page1.screenshot(`${testName}`, `04-fail-${testName}`);
@ -175,7 +175,7 @@ class CustomParameters {
await this.page1.waitForSelector(e.meetingEndedModal); await this.page1.waitForSelector(e.meetingEndedModal);
await this.page1.screenshot(`${testName}`, `04-${testName}`); await this.page1.screenshot(`${testName}`, `04-${testName}`);
await this.page1.logger('audio modal closed'); await this.page1.logger('audio modal closed');
const resp = await this.page1.page.evaluate(checkElementLengthDifferentTo, e.rating, 0); const resp = await this.page1.hasElement(e.rating);
if (!resp) { if (!resp) {
await this.page1.screenshot(`${testName}`, `05-fail-${testName}`); await this.page1.screenshot(`${testName}`, `05-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
@ -199,7 +199,7 @@ class CustomParameters {
await this.page1.screenshot(`${testName}`, `02-${testName}`); await this.page1.screenshot(`${testName}`, `02-${testName}`);
await this.page1.logger('audio modal closed'); await this.page1.logger('audio modal closed');
await this.page1.waitForSelector(e.userListContent); await this.page1.waitForSelector(e.userListContent);
const resp = await this.page1.page.evaluate(checkElementLengthDifferentTo, e.brandingAreaLogo, 0); const resp = await this.page1.hasElement(e.brandingAreaLogo);
if (!resp) { if (!resp) {
await this.page1.screenshot(`${testName}`, `03-fail-${testName}`); await this.page1.screenshot(`${testName}`, `03-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
@ -253,7 +253,7 @@ class CustomParameters {
await this.page1.init(true, true, testName, 'Moderator', undefined, customParameter); await this.page1.init(true, true, testName, 'Moderator', undefined, customParameter);
await this.page1.startRecording(testName); await this.page1.startRecording(testName);
await this.page1.screenshot(`${testName}`, `01-${testName}`); await this.page1.screenshot(`${testName}`, `01-${testName}`);
const resp = await this.page1.page.evaluate(checkElementLengthEqualTo, e.startScreenSharing, 0); const resp = await this.page1.wasRemoved(e.startScreenSharing);
if (!resp) { if (!resp) {
await this.page1.screenshot(`${testName}`, `02-fail-${testName}`); await this.page1.screenshot(`${testName}`, `02-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
@ -274,7 +274,7 @@ class CustomParameters {
await this.page1.init(true, true, testName, 'Moderator', undefined, customParameter); await this.page1.init(true, true, testName, 'Moderator', undefined, customParameter);
await this.page1.startRecording(testName); await this.page1.startRecording(testName);
await this.page1.screenshot(`${testName}`, `01-${testName}`); await this.page1.screenshot(`${testName}`, `01-${testName}`);
const resp = await this.page1.page.evaluate(checkElementLengthEqualTo, e.joinVideo, 0); const resp = await this.page1.wasRemoved(e.joinVideo);
if (!resp) { if (!resp) {
await this.page1.screenshot(`${testName}`, `02-fail-${testName}`); await this.page1.screenshot(`${testName}`, `02-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
@ -457,7 +457,10 @@ class CustomParameters {
await this.page1.screenshot(`${testName}`, `01-${testName}`); await this.page1.screenshot(`${testName}`, `01-${testName}`);
await this.page1.waitForSelector(e.actions); await this.page1.waitForSelector(e.actions);
await this.page1.screenshot(`${testName}`, `02-${testName}`); await this.page1.screenshot(`${testName}`, `02-${testName}`);
const resp = await this.page1.page.evaluate(checkElementLengthDifferentTo, e.defaultContent, 0); const checkPresentationButton = await this.page1.page.evaluate(checkElement, e.restorePresentation);
const checkPresentationPlaceholder = await this.page1.page.evaluate(checkElement, e.presentationPlaceholder);
const resp = !(checkPresentationButton || checkPresentationPlaceholder);
if (!resp) { if (!resp) {
await this.page1.screenshot(`${testName}`, `03-fail-${testName}`); await this.page1.screenshot(`${testName}`, `03-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
@ -480,7 +483,7 @@ class CustomParameters {
await this.page1.screenshot(`${testName}`, `01-${testName}`); await this.page1.screenshot(`${testName}`, `01-${testName}`);
await this.page1.waitForSelector(e.actions); await this.page1.waitForSelector(e.actions);
await this.page1.screenshot(`${testName}`, `02-${testName}`); await this.page1.screenshot(`${testName}`, `02-${testName}`);
const resp = await this.page1.page.evaluate(checkElementLengthDifferentTo, e.notificationBar, 0); const resp = await this.page1.hasElement(e.notificationBar);
if (!resp) { if (!resp) {
await this.page1.screenshot(`${testName}`, `03-fail-${testName}`); await this.page1.screenshot(`${testName}`, `03-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
@ -525,7 +528,7 @@ class CustomParameters {
await this.page1.startRecording(testName); await this.page1.startRecording(testName);
await this.page1.screenshot(`${testName}`, `01-${testName}`); await this.page1.screenshot(`${testName}`, `01-${testName}`);
await this.page1.waitForSelector(e.actions); await this.page1.waitForSelector(e.actions);
const resp = await this.page1.page.evaluate(checkElementLengthDifferentTo, e.restorePresentation, 0) && await this.page1.page.evaluate(checkElementLengthDifferentTo, e.defaultContent, 0); const resp = await this.page1.hasElement(e.restorePresentation) && await this.page1.hasElement(e.defaultContent);
if (!resp) { if (!resp) {
await this.page1.screenshot(`${testName}`, `02-fail-${testName}`); await this.page1.screenshot(`${testName}`, `02-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
@ -592,14 +595,13 @@ class CustomParameters {
await this.page1.screenshot(`${testName}`, `05-page1-${testName}`); await this.page1.screenshot(`${testName}`, `05-page1-${testName}`);
await this.page2.screenshot(`${testName}`, `06-page2-${testName}`); await this.page2.screenshot(`${testName}`, `06-page2-${testName}`);
const test = await this.page2.page.evaluate(checkElementLengthDifferentTo, e.restorePresentation, 0); const test = await this.page2.page.evaluate(checkElement, e.restorePresentation);
const resp = (zoomInCase && zoomOutCase && pollCase && previousSlideCase && nextSlideCase && annotationCase && test); const resp = (zoomInCase && zoomOutCase && pollCase && previousSlideCase && nextSlideCase && annotationCase && test);
if (resp) { if (resp) {
await this.page2.screenshot(`${testName}`, `07-page2-fail-${testName}`); await this.page2.screenshot(`${testName}`, `07-page2-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
return false; return false;
} }
await this.page2.page.evaluate(checkElementLengthEqualTo, e.restorePresentation, 0);
await this.page2.screenshot(`${testName}`, `07-page2-success-${testName}`); await this.page2.screenshot(`${testName}`, `07-page2-success-${testName}`);
await this.page1.logger(testName, ' passed'); await this.page1.logger(testName, ' passed');
@ -624,7 +626,7 @@ class CustomParameters {
await this.page1.screenshot(`${testName}`, `02-page1-${testName}`); await this.page1.screenshot(`${testName}`, `02-page1-${testName}`);
await this.page2.screenshot(`${testName}`, `03-page2-${testName}`); await this.page2.screenshot(`${testName}`, `03-page2-${testName}`);
const test = await this.page2.page.evaluate(checkElementLengthDifferentTo, e.restorePresentation, 0); const test = await this.page2.page.evaluate(checkElement, e.restorePresentation);
if (pollCase && test) { if (pollCase && test) {
await this.page2.screenshot(`${testName}`, `04-page2-fail-${testName}`); await this.page2.screenshot(`${testName}`, `04-page2-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');
@ -688,31 +690,22 @@ class CustomParameters {
await this.page1.init(true, true, testName, 'Moderator1', undefined, customParameter); await this.page1.init(true, true, testName, 'Moderator1', undefined, customParameter);
await this.page1.startRecording(testName); await this.page1.startRecording(testName);
await this.page1.screenshot(`${testName}`, `01-${testName}`); await this.page1.screenshot(`${testName}`, `01-${testName}`);
await this.page1.waitAndClick(e.joinVideo); await this.page1.shareWebcam(false);
const firstCheck = await this.page1.page.evaluate(checkElementLengthEqualTo, e.webcamSettingsModal, 0);
await this.page1.waitAndClick(e.leaveVideo, VIDEO_LOADING_WAIT_TIME); await this.page1.waitAndClick(e.leaveVideo, VIDEO_LOADING_WAIT_TIME);
await this.page1.waitForElementHandleToBeRemoved(e.webcamVideo, ELEMENT_WAIT_LONGER_TIME); await this.page1.waitForElementHandleToBeRemoved(e.webcamVideo, ELEMENT_WAIT_LONGER_TIME);
await this.page1.waitForElementHandleToBeRemoved(e.leaveVideo, ELEMENT_WAIT_LONGER_TIME); await this.page1.waitForElementHandleToBeRemoved(e.leaveVideo, ELEMENT_WAIT_LONGER_TIME);
await this.page1.waitAndClick(e.joinVideo);
const parsedSettings = await this.page1.getSettingsYaml(); const parsedSettings = await this.page1.getSettingsYaml();
const videoPreviewTimeout = parseInt(parsedSettings.public.kurento.gUMTimeout); const videoPreviewTimeout = parseInt(parsedSettings.public.kurento.gUMTimeout);
await this.page1.waitForSelector(e.videoPreview, videoPreviewTimeout); await this.page1.shareWebcam(true, videoPreviewTimeout);
await this.page1.waitForSelector(e.startSharingWebcam);
const secondCheck = await this.page1.page.evaluate(checkElementLengthDifferentTo, e.webcamSettingsModal, 0);
await this.page1.waitAndClick(e.startSharingWebcam);
await this.page1.waitForSelector(e.webcamConnecting);
if (firstCheck !== secondCheck) {
await this.page1.screenshot(`${testName}`, `02-fail-${testName}`);
await this.page1.logger(testName, ' failed');
return false;
}
await this.page1.screenshot(`${testName}`, `02-success-${testName}`); await this.page1.screenshot(`${testName}`, `02-success-${testName}`);
await this.page1.logger(testName, ' passed'); await this.page1.logger(testName, ' passed');
return true; return true;
} catch (err) { } catch (err) {
await this.page1.screenshot(`${testName}`, `02-fail-${testName}`);
await this.page1.logger(err); await this.page1.logger(err);
return false; return false;
} }
@ -726,7 +719,7 @@ class CustomParameters {
await this.page1.waitAndClick(e.joinVideo); await this.page1.waitAndClick(e.joinVideo);
await this.page1.waitForSelector(e.webcamMirroredVideoPreview); await this.page1.waitForSelector(e.webcamMirroredVideoPreview);
await this.page1.waitAndClick(e.startSharingWebcam); await this.page1.waitAndClick(e.startSharingWebcam);
const resp = await this.page1.page.evaluate(checkElementLengthDifferentTo, e.webcamMirroredVideoContainer, 0); const resp = await this.page1.hasElement(e.webcamMirroredVideoContainer);
if (!resp) { if (!resp) {
await this.page1.screenshot(`${testName}`, `02-fail-${testName}`); await this.page1.screenshot(`${testName}`, `02-fail-${testName}`);
await this.page1.logger(testName, ' failed'); await this.page1.logger(testName, ' failed');

View File

@ -116,7 +116,7 @@ class Presentation {
await this.userPage.screenshot(testName, `3-userPage-after-allow-download-and-save-[${this.modPage.meetingId}]`); await this.userPage.screenshot(testName, `3-userPage-after-allow-download-and-save-[${this.modPage.meetingId}]`);
await this.userPage.waitForSelector(e.toastDownload); await this.userPage.waitForSelector(e.toastDownload);
// check download button in presentation after ALLOW it - should be true // check download button in presentation after ALLOW it - should be true
const hasPresentationDownloadBtnAfterAllow = await this.userPage.page.evaluate(checkElement, e.presentationDownloadBtn); const hasPresentationDownloadBtnAfterAllow = await this.userPage.hasElement(e.presentationDownloadBtn);
// disallow the presentation download // disallow the presentation download
await this.modPage.waitAndClick(e.actions); await this.modPage.waitAndClick(e.actions);

View File

@ -2,11 +2,11 @@ const Page = require('../core/page');
const e = require('../core/elements'); const e = require('../core/elements');
const c = require('../core/constants'); const c = require('../core/constants');
const util = require('./util'); const util = require('./util');
const { checkElementLengthEqualTo } = require('../core/util');
class Stress extends Page { class Stress {
constructor() { constructor() {
super(); this.modPage = new Page();
this.userPages = [];
} }
async moderatorAsPresenter(testName) { async moderatorAsPresenter(testName) {
@ -14,26 +14,97 @@ class Stress extends Page {
const maxFailRate = c.JOIN_AS_MODERATOR_TEST_ROUNDS * c.MAX_JOIN_AS_MODERATOR_FAIL_RATE; const maxFailRate = c.JOIN_AS_MODERATOR_TEST_ROUNDS * c.MAX_JOIN_AS_MODERATOR_FAIL_RATE;
let failureCount = 0; let failureCount = 0;
for (let i = 1; i <= c.JOIN_AS_MODERATOR_TEST_ROUNDS; i++) { for (let i = 1; i <= c.JOIN_AS_MODERATOR_TEST_ROUNDS; i++) {
await this.init(true, true, testName, `Moderator-${i}`); await this.modPage.init(true, true, testName, `Moderator-${i}`);
await this.waitForSelector(e.userAvatar); await this.modPage.waitForSelector(e.userAvatar);
const hasPresenterClass = await this.page.evaluate(util.checkIncludeClass, e.userAvatar, e.presenterClassName); const hasPresenterClass = await this.modPage.page.evaluate(util.checkIncludeClass, e.userAvatar, e.presenterClassName);
await this.waitAndClick(e.actions); await this.modPage.waitAndClick(e.actions);
const canStartPoll = await this.page.evaluate(checkElementLengthEqualTo, e.polling, 1); const canStartPoll = await this.modPage.hasElement(e.polling);
if (!hasPresenterClass || !canStartPoll) { if (!hasPresenterClass || !canStartPoll) {
failureCount++; failureCount++;
await this.screenshot(testName, `loop-${i}-failure-${testName}`); await this.modPage.screenshot(testName, `loop-${i}-failure-${testName}`);
} }
await this.close(); await this.modPage.close();
await this.logger(`Loop ${i} of ${c.JOIN_AS_MODERATOR_TEST_ROUNDS} completed`); await this.modPage.logger(`Loop ${i} of ${c.JOIN_AS_MODERATOR_TEST_ROUNDS} completed`);
if (failureCount > maxFailRate) return false; if (failureCount > maxFailRate) return false;
} }
return true; return true;
} catch (err) { } catch (err) {
await this.close(); await this.modPage.logger(err);
this.logger(err);
return false; return false;
} }
} }
async breakoutRoomInvitation(testName) {
try {
await this.modPage.init(true, true, testName, 'Moderator');
for (let i = 1; i <= c.BREAKOUT_ROOM_INVITATION_TEST_ROUNDS; i++) {
const userName = `User-${i}`;
const userPage = new Page();
await userPage.init(false, true, testName, userName, this.modPage.meetingId);
await userPage.logger(`${userName} joined`);
this.userPages.push(userPage);
}
// Create breakout rooms with the allow choice option enabled
await this.modPage.bringToFront();
await this.modPage.waitAndClick(e.manageUsers);
await this.modPage.waitAndClick(e.createBreakoutRooms);
await this.modPage.waitAndClick(e.allowChoiceRoom);
await this.modPage.screenshot(testName, '01-modPage-before-create-breakout-rooms-allowing-choice');
await this.modPage.waitAndClick(e.modalConfirmButton);
for (const page of this.userPages) {
await page.bringToFront();
const firstCheck = await page.hasElement(e.modalConfirmButton, c.ELEMENT_WAIT_LONGER_TIME);
const secondCheck = await page.wasRemoved(e.labelGeneratingURL, c.ELEMENT_WAIT_LONGER_TIME);
if (!firstCheck || !secondCheck) {
await page.screenshot(testName, `${page.effectiveParams.fullName}-breakout-modal-failed`);
return false;
}
await page.screenshot(testName, `${page.effectiveParams.fullName}-breakout-modal-allowing-choice-success`);
}
// End breakout rooms
await this.modPage.bringToFront();
await this.modPage.waitAndClick(e.breakoutRoomsItem);
await this.modPage.waitAndClick(e.endBreakoutRoomsButton);
await this.modPage.closeAudioModal();
// Create breakout rooms with the allow choice option NOT enabled (randomly assign)
await this.modPage.waitAndClick(e.manageUsers);
await this.modPage.waitAndClick(e.createBreakoutRooms);
await this.modPage.waitAndClick(e.randomlyAssign);
await this.modPage.screenshot(testName, '02-modPage-before-create-breakout-rooms-not-allowing-choice');
await this.modPage.waitAndClick(e.modalConfirmButton);
for (const page of this.userPages) {
await page.bringToFront();
const check = await page.hasElement(e.modalConfirmButton);
if (!check) {
await page.screenshot(testName, `${page.effectiveParams.fullName}-breakout-modal-not-allowing-choose-failed`);
return false;
}
await page.screenshot(testName, `${page.effectiveParams.fullName}-breakout-modal-not-allowing-choose-success`);
}
return true;
} catch (err) {
await this.modPage.logger(err);
return false;
}
}
async closeUserPages() {
for (const page of this.userPages) {
try {
await page.close();
} catch (err) {
await this.modPage.logger(err);
}
}
}
} }
module.exports = exports = Stress; module.exports = exports = Stress;

View File

@ -7,11 +7,31 @@ const stressTest = () => {
let response; let response;
try { try {
const testName = 'firstModeratorAsPresenter'; const testName = 'firstModeratorAsPresenter';
await test.logger('begin of ', testName); await test.modPage.logger('begin of ', testName);
response = await test.moderatorAsPresenter(testName); response = await test.moderatorAsPresenter(testName);
await test.logger('end of ', testName); await test.modPage.logger('end of ', testName);
} catch (err) { } catch (err) {
await test.logger(err); await test.modPage.logger(err);
} finally {
await test.modPage.close();
}
expect(response).toBe(true);
});
// Check that all users invited to a breakout room can join it
test('All users must receive breakout room invitations', async () => {
const test = new Stress();
let response;
try {
const testName = 'breakoutRoomInvitation';
await test.modPage.logger('begin of ', testName);
response = await test.breakoutRoomInvitation(testName);
await test.modPage.logger('end of ', testName);
} catch (err) {
await test.modPage.logger(err);
} finally {
await test.modPage.close();
await test.closeUserPages();
} }
expect(response).toBe(true); expect(response).toBe(true);
}); });

View File

@ -3,7 +3,6 @@ const { exec } = require("child_process");
const { CLIENT_RECONNECTION_TIMEOUT } = require('../core/constants'); // core constants (Timeouts vars imported) const { CLIENT_RECONNECTION_TIMEOUT } = require('../core/constants'); // core constants (Timeouts vars imported)
const { sleep } = require('../core/helper'); const { sleep } = require('../core/helper');
const e = require('../core/elements'); const e = require('../core/elements');
const { checkElementLengthDifferentTo } = require('../core/util');
class Trigger extends Page { class Trigger extends Page {
constructor() { constructor() {
@ -30,7 +29,7 @@ class Trigger extends Page {
await sleep(3000); await sleep(3000);
await this.screenshot(`${testName}`, `03-after-meteor-reconnection-[${this.meetingId}]`); await this.screenshot(`${testName}`, `03-after-meteor-reconnection-[${this.meetingId}]`);
const findUnauthorized = await this.page.evaluate(checkElementLengthDifferentTo, e.unauthorized, 0) === true; const findUnauthorized = await this.hasElement(e.unauthorized);
await this.logger('Check if Unauthorized message appears => ', findUnauthorized); await this.logger('Check if Unauthorized message appears => ', findUnauthorized);
return meteorStatusConfirm && getAudioButton && findUnauthorized; return meteorStatusConfirm && getAudioButton && findUnauthorized;
} catch (err) { } catch (err) {
@ -84,7 +83,7 @@ class Trigger extends Page {
}, e.joinAudio) }, e.joinAudio)
await this.logger('Check if Connections Buttons are disabled => ', getAudioButton); await this.logger('Check if Connections Buttons are disabled => ', getAudioButton);
await sleep(3000); await sleep(3000);
const findUnauthorized = await this.page.evaluate(checkElementLengthDifferentTo, e.unauthorized, 0) === true; const findUnauthorized = await this.hasElement(e.unauthorized);
await this.logger('Check if Unauthorized message appears => ', findUnauthorized); await this.logger('Check if Unauthorized message appears => ', findUnauthorized);
return meteorStatusConfirm && getAudioButton && findUnauthorized; return meteorStatusConfirm && getAudioButton && findUnauthorized;
} catch (err) { } catch (err) {

View File

@ -2,8 +2,8 @@ const Page = require('../core/page');
const util = require('../chat/util'); const util = require('../chat/util');
const utilUser = require('./util'); const utilUser = require('./util');
const e = require('../core/elements'); const e = require('../core/elements');
const { ELEMENT_WAIT_TIME } = require('../core/constants'); const { ELEMENT_WAIT_TIME, ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const { getElementLength, checkElementLengthEqualTo, checkElementLengthDifferentTo } = require('../core/util'); const { getElementLength, checkElementLengthEqualTo } = require('../core/util');
class MultiUsers { class MultiUsers {
constructor() { constructor() {
@ -293,7 +293,7 @@ class MultiUsers {
await this.page2.close(); await this.page2.close();
await utilUser.connectionStatus(this.page1); await utilUser.connectionStatus(this.page1);
const connectionStatusItemEmpty = await this.page1.wasRemoved(e.connectionStatusItemEmpty); const connectionStatusItemEmpty = await this.page1.wasRemoved(e.connectionStatusItemEmpty);
const connectionStatusOfflineUser = await this.page1.hasElement(e.connectionStatusOfflineUser, true); const connectionStatusOfflineUser = await this.page1.hasElement(e.connectionStatusOfflineUser, true, ELEMENT_WAIT_LONGER_TIME);
return connectionStatusItemEmpty && connectionStatusOfflineUser; return connectionStatusItemEmpty && connectionStatusOfflineUser;
} catch (err) { } catch (err) {
@ -313,13 +313,58 @@ class MultiUsers {
} }
} }
async usersConnectionStatus(testName) {
try {
await this.page1.shareWebcam(true);
await this.page1.screenshot(testName, '01-page1-after-share-webcam');
await this.initUserPage(false, testName);
await this.userPage.joinMicrophone();
await this.userPage.screenshot(testName, '02-userPage-after-join-microhpone');
await this.userPage.shareWebcam(true);
await this.userPage.screenshot(testName, '03-userPage-after-share-webcam');
await this.userPage.waitAndClick(e.connectionStatusBtn);
try {
await this.userPage.page.waitForFunction(utilUser.checkNetworkStatus, { timeout: ELEMENT_WAIT_TIME },
e.connectionDataContainer, e.connectionNetwordData
);
await this.userPage.screenshot(testName, '04-connection-network-success');
return true;
} catch (err) {
await this.userPage.screenshot(testName, '04-connection-network-failed');
this.userPage.logger(err);
return false;
}
} catch (err) {
this.page1.logger(err);
return false;
}
}
async disableWebcamsFromConnectionStatus() {
try {
await this.page1.shareWebcam(true, ELEMENT_WAIT_LONGER_TIME);
await this.page2.shareWebcam(true, ELEMENT_WAIT_LONGER_TIME);
await utilUser.connectionStatus(this.page1);
await this.page1.waitAndClickElement(e.dataSavingWebcams);
await this.page1.waitAndClickElement(e.closeConnectionStatusModal);
await this.page1.waitForSelector(e.smallToastMsg);
const checkUserWhoHasDisabled = await this.page1.page.evaluate(checkElementLengthEqualTo, e.videoContainer, 1);
const checkSecondUser = await this.page2.page.evaluate(checkElementLengthEqualTo, e.videoContainer, 2);
return checkUserWhoHasDisabled && checkSecondUser;
} catch (err) {
await this.page1.logger(err);
return false;
}
}
async whiteboardNotAppearOnMobile() { async whiteboardNotAppearOnMobile() {
try { try {
await this.page1.waitAndClick(e.userListButton); await this.page1.waitAndClick(e.userListButton);
await this.page2.waitAndClick(e.userListButton); await this.page2.waitAndClick(e.userListButton);
await this.page2.waitAndClick(e.chatButtonKey); await this.page2.waitAndClick(e.chatButtonKey);
const onUserListPanel = await this.page1.isNotVisible(e.hidePresentation); const onUserListPanel = await this.page1.wasRemoved(e.hidePresentation);
const onChatPanel = await this.page2.isNotVisible(e.hidePresentation); const onChatPanel = await this.page2.wasRemoved(e.hidePresentation);
return onUserListPanel && onChatPanel; return onUserListPanel && onChatPanel;
} catch (err) { } catch (err) {
@ -333,7 +378,7 @@ class MultiUsers {
await this.page2.waitAndClick(e.userListButton); await this.page2.waitAndClick(e.userListButton);
await this.page2.waitAndClick(e.chatButtonKey); await this.page2.waitAndClick(e.chatButtonKey);
const whiteboard = await this.page1.page.evaluate(checkElementLengthEqualTo, e.chatButtonKey, 0); const whiteboard = await this.page1.page.evaluate(checkElementLengthEqualTo, e.chatButtonKey, 0);
const onChatPanel = await this.page2.isNotVisible(e.chatButtonKey); const onChatPanel = await this.page2.hasElement(e.chatButtonKey, false);
return whiteboard && onChatPanel; return whiteboard && onChatPanel;
} catch (err) { } catch (err) {

View File

@ -2,10 +2,9 @@ const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants');
const Page = require('../core/page'); const Page = require('../core/page');
const e = require('../core/elements'); const e = require('../core/elements');
const util = require('./util'); const util = require('./util');
const utilWebcam = require('../webcam/util');
const utilScreenshare = require('../screenshare/util'); const utilScreenshare = require('../screenshare/util');
const { sleep } = require('../core/helper'); const { sleep } = require('../core/helper');
const { checkElementLengthEqualTo, checkElementLengthDifferentTo } = require('../core/util'); const { checkElementLengthEqualTo } = require('../core/util');
class Status extends Page { class Status extends Page {
constructor() { constructor() {
@ -15,12 +14,11 @@ class Status extends Page {
async test() { async test() {
try { try {
await util.setStatus(this, e.applaud); await util.setStatus(this, e.applaud);
const resp1 = await this.page.evaluate(checkElementLengthDifferentTo, e.applauseIcon, 0); const resp1 = await this.hasElement(e.applauseIcon);
await sleep(1000);
await util.setStatus(this, e.away); await util.setStatus(this, e.away);
const resp2 = await this.page.evaluate(checkElementLengthDifferentTo, e.awayIcon, 0); const resp2 = await this.hasElement(e.awayIcon);
await this.waitAndClick(e.firstUser);
await this.waitAndClick(e.clearStatus);
return resp1 === resp2; return resp1 === resp2;
} catch (err) { } catch (err) {
await this.logger(err); await this.logger(err);
@ -33,7 +31,7 @@ class Status extends Page {
await this.waitAndClick(e.userList); await this.waitAndClick(e.userList);
await this.waitForSelector(e.firstUser); await this.waitForSelector(e.firstUser);
const response = await this.page.evaluate(checkElementLengthDifferentTo, e.mobileUser, 0); const response = await this.hasElement(e.mobileUser);
return response === true; return response === true;
} catch (err) { } catch (err) {
await this.logger(err); await this.logger(err);
@ -44,7 +42,7 @@ class Status extends Page {
async findConnectionStatusModal() { async findConnectionStatusModal() {
try { try {
await util.connectionStatus(this); await util.connectionStatus(this);
const resp = await this.page.evaluate(checkElementLengthDifferentTo, e.connectionStatusModal, 0); const resp = await this.hasElement(e.connectionStatusModal);
return resp === true; return resp === true;
} catch (err) { } catch (err) {
await this.logger(err); await this.logger(err);
@ -52,21 +50,6 @@ class Status extends Page {
} }
} }
async disableWebcamsFromConnectionStatus() {
try {
await utilWebcam.enableWebcam(this, ELEMENT_WAIT_LONGER_TIME);
await util.connectionStatus(this);
await this.waitAndClickElement(e.dataSavingWebcams);
await this.waitAndClickElement(e.closeConnectionStatusModal);
await sleep(2000);
const webcamsIsDisabledInDataSaving = await this.page.evaluate(checkElementLengthDifferentTo, e.webcamsIsDisabledInDataSaving, 0);
return webcamsIsDisabledInDataSaving === true;
} catch (err) {
await this.logger(err);
return false;
}
}
async disableScreenshareFromConnectionStatus() { async disableScreenshareFromConnectionStatus() {
try { try {
await utilScreenshare.startScreenshare(this); await utilScreenshare.startScreenshare(this);
@ -74,8 +57,8 @@ class Status extends Page {
await util.connectionStatus(this); await util.connectionStatus(this);
await this.waitAndClickElement(e.dataSavingScreenshare); await this.waitAndClickElement(e.dataSavingScreenshare);
await this.waitAndClickElement(e.closeConnectionStatusModal); await this.waitAndClickElement(e.closeConnectionStatusModal);
await sleep(2000);
const webcamsIsDisabledInDataSaving = await this.page.evaluate(checkElementLengthEqualTo, e.screenshareLocked, 0); const webcamsIsDisabledInDataSaving = await this.hasElement(e.screenshareLocked);
return webcamsIsDisabledInDataSaving === true; return webcamsIsDisabledInDataSaving === true;
} catch (err) { } catch (err) {
await this.logger(err); await this.logger(err);
@ -87,13 +70,13 @@ class Status extends Page {
try { try {
await this.page.evaluate(() => window.dispatchEvent(new CustomEvent('socketstats', { detail: { rtt: 2000 } }))); await this.page.evaluate(() => window.dispatchEvent(new CustomEvent('socketstats', { detail: { rtt: 2000 } })));
await this.joinMicrophone(); await this.joinMicrophone();
await utilWebcam.enableWebcam(this, ELEMENT_WAIT_LONGER_TIME); await this.shareWebcam(true, ELEMENT_WAIT_LONGER_TIME);
await utilScreenshare.startScreenshare(this); await utilScreenshare.startScreenshare(this);
await utilScreenshare.waitForScreenshareContainer(this); await utilScreenshare.waitForScreenshareContainer(this);
await util.connectionStatus(this); await util.connectionStatus(this);
await sleep(5000); await sleep(5000);
const connectionStatusItemEmpty = await this.page.evaluate(checkElementLengthEqualTo, e.connectionStatusItemEmpty, 0); const connectionStatusItemEmpty = await this.page.evaluate(checkElementLengthEqualTo, e.connectionStatusItemEmpty, 0);
const connectionStatusItemUser = await this.page.evaluate(checkElementLengthDifferentTo, e.connectionStatusItemUser, 0); const connectionStatusItemUser = await this.hasElement(e.connectionStatusItemUser);
return connectionStatusItemUser && connectionStatusItemEmpty; return connectionStatusItemUser && connectionStatusItemEmpty;
} catch (err) { } catch (err) {
await this.logger(err); await this.logger(err);

View File

@ -112,22 +112,22 @@ const userTest = () => {
// Open Connection Status Modal, start Webcam Share, disable Webcams in // Open Connection Status Modal, start Webcam Share, disable Webcams in
// Connection Status Modal and check if webcam sharing is still available // Connection Status Modal and check if webcam sharing is still available
test('Disable Webcams From Connection Status Modal', async () => { test('Disable Webcams From Connection Status Modal', async () => {
const test = new Status(); const test = new MultiUsers();
let response; let response;
let screenshot; let screenshot;
try { try {
const testName = 'disableWebcamsFromConnectionStatus'; const testName = 'disableWebcamsFromConnectionStatus';
await test.logger('begin of ', testName); await test.page1.logger('begin of ', testName);
await test.init(true, true, testName); await test.init(testName);
await test.startRecording(testName); await test.page1.startRecording(testName);
response = await test.disableWebcamsFromConnectionStatus(); response = await test.disableWebcamsFromConnectionStatus();
await test.stopRecording(); await test.page1.stopRecording();
screenshot = await test.page.screenshot(); screenshot = await test.page1.screenshot();
await test.logger('end of ', testName); await test.page1.logger('end of ', testName);
} catch (err) { } catch (err) {
await test.logger(err); await test.page1.logger(err);
} finally { } finally {
await test.close(); await test.close(test.page1, test.page2);
} }
expect(response).toBe(true); expect(response).toBe(true);
Page.checkRegression(2.0, screenshot); Page.checkRegression(2.0, screenshot);
@ -205,6 +205,29 @@ const userTest = () => {
Page.checkRegression(2.0, screenshot); Page.checkRegression(2.0, screenshot);
}, TEST_DURATION_TIME); }, TEST_DURATION_TIME);
test('Show network data in Connection Status', async () => {
const test = new MultiUsers();
let response;
let screenshot;
try {
const testName = 'connectionNetworkStatus';
await test.page1.logger('begin of ', testName);
await test.initMod1(testName);
await test.page1.startRecording(testName);
response = await test.usersConnectionStatus(testName);
await test.page1.stopRecording();
screenshot = await test.page1.page.screenshot();
await test.page1.logger('end of ', testName);
} catch (err) {
await test.page1.logger(err);
} finally {
await test.close(test.page1, test.userPage);
}
expect(response).toBe(true);
Page.checkRegression(2.0, screenshot);
});
// Raise and Lower Hand and make sure that the User2 Avatar color // Raise and Lower Hand and make sure that the User2 Avatar color
// and its avatar in raised hand toast are the same // and its avatar in raised hand toast are the same
test('Raise Hand Toast', async () => { test('Raise Hand Toast', async () => {

View File

@ -11,5 +11,15 @@ async function connectionStatus(test) {
await test.waitForSelector(e.connectionStatusModal); await test.waitForSelector(e.connectionStatusModal);
} }
function checkNetworkStatus(dataContainer, networdData) {
const values = Array.from(document.querySelectorAll(`${dataContainer} > ${networdData}`));
values.splice(4, values.length - 4);
const check = values.filter(e => e.textContent.includes(' 0 k'))[0];
if (!check) return true;
}
exports.setStatus = setStatus; exports.setStatus = setStatus;
exports.connectionStatus = connectionStatus; exports.connectionStatus = connectionStatus;
exports.checkNetworkStatus = checkNetworkStatus;

View File

@ -22,7 +22,7 @@ class Check extends Share {
const parsedSettings = await this.getSettingsYaml(); const parsedSettings = await this.getSettingsYaml();
const videoPreviewTimeout = parseInt(parsedSettings.public.kurento.gUMTimeout); const videoPreviewTimeout = parseInt(parsedSettings.public.kurento.gUMTimeout);
await util.enableWebcam(this, videoPreviewTimeout); await this.shareWebcam(true, videoPreviewTimeout);
const respUser = await util.webcamContentCheck(this); const respUser = await util.webcamContentCheck(this);
return respUser === true; return respUser === true;
} catch (err) { } catch (err) {

View File

@ -1,7 +1,5 @@
const Page = require('../core/page'); const Page = require('../core/page');
const util = require('./util');
const e = require('../core/elements'); const e = require('../core/elements');
const { checkElementLengthDifferentTo } = require('../core/util');
const { VIDEO_LOADING_WAIT_TIME } = require('../core/constants'); // core constants (Timeouts vars imported) const { VIDEO_LOADING_WAIT_TIME } = require('../core/constants'); // core constants (Timeouts vars imported)
class Share extends Page { class Share extends Page {
@ -13,8 +11,9 @@ class Share extends Page {
try { try {
const parsedSettings = await this.getSettingsYaml(); const parsedSettings = await this.getSettingsYaml();
const videoPreviewTimeout = parseInt(parsedSettings.public.kurento.gUMTimeout); const videoPreviewTimeout = parseInt(parsedSettings.public.kurento.gUMTimeout);
const response = await util.enableWebcam(this, videoPreviewTimeout); await this.shareWebcam(true, videoPreviewTimeout);
return response;
return true;
} catch (err) { } catch (err) {
await this.logger(err); await this.logger(err);
return false; return false;
@ -26,7 +25,7 @@ class Share extends Page {
await this.joinMicrophone(); await this.joinMicrophone();
const parsedSettings = await this.getSettingsYaml(); const parsedSettings = await this.getSettingsYaml();
const videoPreviewTimeout = parseInt(parsedSettings.public.kurento.gUMTimeout); const videoPreviewTimeout = parseInt(parsedSettings.public.kurento.gUMTimeout);
await util.enableWebcam(this, videoPreviewTimeout); await this.shareWebcam(true, videoPreviewTimeout);
} catch (err) { } catch (err) {
await this.logger(err); await this.logger(err);
} }
@ -37,7 +36,7 @@ class Share extends Page {
await this.waitForSelector(e.webcamVideo, VIDEO_LOADING_WAIT_TIME); await this.waitForSelector(e.webcamVideo, VIDEO_LOADING_WAIT_TIME);
await this.waitForSelector(e.leaveVideo, VIDEO_LOADING_WAIT_TIME); await this.waitForSelector(e.leaveVideo, VIDEO_LOADING_WAIT_TIME);
await this.waitForSelector(e.isTalking); await this.waitForSelector(e.isTalking);
const foundTestElement = await this.page.evaluate(checkElementLengthDifferentTo, e.webcamItemTalkingUser, 0); const foundTestElement = await this.hasElement(e.webcamItemTalkingUser);
if (foundTestElement === true) { if (foundTestElement === true) {
await this.screenshot(`${testName}`, `success-${testName}`); await this.screenshot(`${testName}`, `success-${testName}`);
this.logger(testName, ' passed'); this.logger(testName, ' passed');

View File

@ -1,23 +1,11 @@
const e = require('../core/elements'); const e = require('../core/elements');
const { sleep } = require('../core/helper'); const { sleep } = require('../core/helper');
const { checkElement, checkElementLengthDifferentTo } = require('../core/util'); const { checkElement } = require('../core/util');
const { const {
LOOP_INTERVAL, LOOP_INTERVAL,
VIDEO_LOADING_WAIT_TIME,
ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_LONGER_TIME,
} = require('../core/constants'); } = require('../core/constants');
async function enableWebcam(test, videoPreviewTimeout) {
// Enabling webcam
await test.waitAndClick(e.joinVideo);
await test.waitForSelector(e.videoPreview, videoPreviewTimeout);
await test.waitAndClick(e.startSharingWebcam);
await test.waitForSelector(e.webcamConnecting);
await test.waitForSelector(e.webcamVideo, VIDEO_LOADING_WAIT_TIME);
await test.waitForSelector(e.leaveVideo, VIDEO_LOADING_WAIT_TIME);
return test.page.evaluate(checkElementLengthDifferentTo, e.webcamVideo, 0);
}
async function evaluateCheck(test) { async function evaluateCheck(test) {
await test.waitForSelector(e.videoContainer); await test.waitForSelector(e.videoContainer);
return test.page.evaluate(checkElement, e.presentationFullscreenButton, 1); return test.page.evaluate(checkElement, e.presentationFullscreenButton, 1);
@ -69,4 +57,3 @@ async function webcamContentCheck(test) {
exports.startAndCheckForWebcams = startAndCheckForWebcams; exports.startAndCheckForWebcams = startAndCheckForWebcams;
exports.webcamContentCheck = webcamContentCheck; exports.webcamContentCheck = webcamContentCheck;
exports.evaluateCheck = evaluateCheck; exports.evaluateCheck = evaluateCheck;
exports.enableWebcam = enableWebcam;

View File

@ -19,7 +19,6 @@ const webcamTest = () => {
const testName = 'shareWebcam'; const testName = 'shareWebcam';
await test.logger('begin of ', testName); await test.logger('begin of ', testName);
await test.init(true, true, testName); await test.init(true, true, testName);
await test.closeAudioModal();
await test.startRecording(testName); await test.startRecording(testName);
response = await test.test(); response = await test.test();
await test.stopRecording(); await test.stopRecording();
@ -42,7 +41,6 @@ const webcamTest = () => {
const testName = 'checkWebcamContent'; const testName = 'checkWebcamContent';
await test.logger('begin of ', testName); await test.logger('begin of ', testName);
await test.init(true, true, testName); await test.init(true, true, testName);
await test.closeAudioModal();
await test.startRecording(testName); await test.startRecording(testName);
response = await test.test(); response = await test.test();
await test.stopRecording(); await test.stopRecording();

View File

@ -1056,6 +1056,120 @@ class ApiController {
} }
} }
/***********************************************
* LEARNING DASHBOARD DATA
***********************************************/
def learningDashboard = {
String API_CALL = 'learningDashboard'
log.debug CONTROLLER_NAME + "#${API_CALL}"
String respMessage = ""
boolean reject = false
String sessionToken
UserSession us
Meeting meeting
String validationResponse = validateRequest(
ValidationService.ApiCall.ENTER,
request.getParameterMap(),
request.getQueryString(),
)
//Validate Session
if(!validationResponse.isEmpty()) {
respMessage = validationResponse
reject = true
} else {
sessionToken = sanitizeSessionToken(params.sessionToken)
if (!hasValidSession(sessionToken)) {
reject = true
respMessage = "Invalid Session"
}
}
//Validate User
if(reject == false) {
us = getUserSession(sessionToken)
if(us == null) {
reject = true;
respMessage = "Access denied"
} else if(!us.role.equals(ROLE_MODERATOR)) {
reject = true
respMessage = "Access denied"
}
}
//Validate Meeting
if(reject == false) {
meeting = meetingService.getMeeting(us.meetingID)
boolean isRunning = meeting != null && meeting.isRunning();
if(!isRunning) {
reject = true
respMessage = "Meeting not found"
}
if(meeting.getLearningDashboardEnabled() == false) {
reject = true
respMessage = "Learning Dashboard disabled for this meeting"
}
}
//Validate File
File jsonDataFile
if(reject == false) {
jsonDataFile = meetingService.learningDashboardService.getJsonDataFile(us.meetingID,meeting.getLearningDashboardAccessToken());
if (!jsonDataFile.exists()) {
reject = true
respMessage = "Learning Dashboard data not found"
}
}
if (reject) {
response.addHeader("Cache-Control", "no-cache")
withFormat {
json {
def builder = new JsonBuilder()
builder.response {
returncode RESP_CODE_FAILED
message respMessage
sessionToken
}
render(contentType: "application/json", text: builder.toPrettyString())
}
}
} else {
Map<String, Object> logData = new HashMap<String, Object>();
logData.put("meetingid", us.meetingID);
logData.put("extMeetingid", us.externMeetingID);
logData.put("name", us.fullname);
logData.put("userid", us.internalUserId);
logData.put("sessionToken", sessionToken);
logData.put("logCode", "learningDashboard");
logData.put("description", "Request Learning Dashboard data.");
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.info(" --analytics-- data=" + logStr);
response.addHeader("Cache-Control", "no-cache")
withFormat {
json {
def builder = new JsonBuilder()
builder.response {
returncode RESP_CODE_SUCCESS
data jsonDataFile.getText()
sessionToken
}
render(contentType: "application/json", text: builder.toPrettyString())
}
}
}
}
def uploadDocuments(conf) { // def uploadDocuments(conf) { //
log.debug("ApiController#uploadDocuments(${conf.getInternalId()})"); log.debug("ApiController#uploadDocuments(${conf.getInternalId()})");

View File

@ -151,6 +151,30 @@
} }
}, },
"response": [] "response": []
},
{
"name": "learningDashboard",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/{{path}}/learningDashboard?{{param_session_token}}=",
"host": [
"{{base_url}}"
],
"path": [
"{{path}}",
"learningDashboard"
],
"query": [
{
"key": "{{param_session_token}}",
"value": ""
}
]
}
},
"response": []
} }
] ]
} }

View File

@ -108,6 +108,20 @@ if [ -f /usr/lib/systemd/system/red5.service ]; then
chown root:root /usr/lib/systemd/system/red5.service chown root:root /usr/lib/systemd/system/red5.service
fi fi
# Verify mediasoup raw media directories ownership and perms
if [ -d /var/mediasoup ]; then
chown bigbluebutton:bigbluebutton /var/mediasoup
chmod 0700 /var/mediasoup
fi
if [ -d /var/mediasoup/recordings ]; then
chmod 0700 /var/mediasoup/recordings
fi
if [ -d /var/mediasoup/screenshare ]; then
chmod 0700 /var/mediasoup/screenshare
fi
sed -i 's/worker_connections 768/worker_connections 4000/g' /etc/nginx/nginx.conf sed -i 's/worker_connections 768/worker_connections 4000/g' /etc/nginx/nginx.conf
if ! grep "worker_rlimit_nofile 10000;" /etc/nginx/nginx.conf; then if ! grep "worker_rlimit_nofile 10000;" /etc/nginx/nginx.conf; then

View File

@ -32,7 +32,9 @@ git clone https://github.com/mconf/ep_redis_publisher.git
npm pack ./ep_redis_publisher npm pack ./ep_redis_publisher
npm install ./ep_redis_publisher-*.tgz npm install ./ep_redis_publisher-*.tgz
npm install ep_cursortrace # npm install ep_cursortrace
# using mconf's fork due to https://github.com/ether/ep_cursortrace/pull/25 not being accepted upstream
npm install git+https://github.com/mconf/ep_cursortrace.git
npm install ep_disable_chat npm install ep_disable_chat
# For some reason installing from github using npm 7.5.2 gives # For some reason installing from github using npm 7.5.2 gives

View File

@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
/opt/freeswitch/bin/fs_cli -p $(xmlstarlet sel -t -m 'configuration/settings/param[@name="password"]' -v @value /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml) # "$@" is placed at the end of the command so it can be used as "fs_clibbb -x 'show channels as json'"
# @ will be replaced by the arguments you pass in the command line.
/opt/freeswitch/bin/fs_cli -p $(xmlstarlet sel -t -m 'configuration/settings/param[@name="password"]' -v @value /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml) "$@"

View File

@ -18,6 +18,14 @@ location /html5client/fonts {
alias /usr/share/meteor/bundle/programs/web.browser/app/fonts; alias /usr/share/meteor/bundle/programs/web.browser/app/fonts;
} }
location /html5client/wasm {
types {
application/wasm wasm;
}
gzip_static on;
alias /usr/share/meteor/bundle/programs/web.browser/app/wasm;
}
location ~ ^/html5client/ { location ~ ^/html5client/ {
# proxy_pass http://127.0.0.1:4100; # use for development # proxy_pass http://127.0.0.1:4100; # use for development
proxy_pass http://poolhtml5servers; # use for production proxy_pass http://poolhtml5servers; # use for production

View File

@ -84,6 +84,16 @@ if [ -f staging/usr/share/meteor/bundle/programs/web.browser/head.html ]; then
sed -i "s/VERSION/$(($BUILD))/" staging/usr/share/meteor/bundle/programs/web.browser/head.html sed -i "s/VERSION/$(($BUILD))/" staging/usr/share/meteor/bundle/programs/web.browser/head.html
fi fi
# Compress tensorflow WASM binaries used for virtual backgrounds. Keep the
# uncompressed versions as well so it works with mismatched nginx location blocks
if [ -f staging/usr/share/meteor/bundle/programs/web.browser/app/wasm/tflite-simd.wasm ]; then
gzip -k -f -9 staging/usr/share/meteor/bundle/programs/web.browser/app/wasm/tflite-simd.wasm
fi
if [ -f staging/usr/share/meteor/bundle/programs/web.browser/app/wasm/tflite.wasm ]; then
gzip -k -f -9 staging/usr/share/meteor/bundle/programs/web.browser/app/wasm/tflite.wasm
fi
mkdir -p staging/etc/nginx/sites-available mkdir -p staging/etc/nginx/sites-available
cp bigbluebutton.nginx staging/etc/nginx/sites-available/bigbluebutton cp bigbluebutton.nginx staging/etc/nginx/sites-available/bigbluebutton

View File

@ -101,6 +101,8 @@ bbb_config() {
touch /var/log/bigbluebutton/bbb-web.log touch /var/log/bigbluebutton/bbb-web.log
chown bigbluebutton:bigbluebutton /var/log/bigbluebutton/bbb-web.log chown bigbluebutton:bigbluebutton /var/log/bigbluebutton/bbb-web.log
update-java-alternatives -s java-1.8.0-openjdk-amd64
# Restart bbb-web to deploy new # Restart bbb-web to deploy new
startService bbb-web.service || echo "bbb-web.service could not be registered or started" startService bbb-web.service || echo "bbb-web.service could not be registered or started"
# sed -i 's/8080/8090/g' /etc/bigbluebutton/nginx/web # sed -i 's/8080/8090/g' /etc/bigbluebutton/nginx/web

View File

@ -1,3 +1,3 @@
. ./opts-global.sh . ./opts-global.sh
OPTS="$OPTS -t deb -d zip,unzip,imagemagick,redis-server,xpdf-utils,bbb-libreoffice-docker,psmisc,fonts-crosextra-carlito,fonts-crosextra-caladea,fonts-noto --deb-user bigbluebutton --deb-group bigbluebutton --deb-use-file-permissions" OPTS="$OPTS -t deb -d zip,unzip,imagemagick,redis-server,xpdf-utils,bbb-libreoffice-docker,psmisc,fonts-crosextra-carlito,fonts-crosextra-caladea,fonts-noto,openjdk-8-jdk --deb-user bigbluebutton --deb-group bigbluebutton --deb-use-file-permissions"

View File

@ -16,6 +16,10 @@ case "$1" in
# https://github.com/bigbluebutton/bbb-webrtc-sfu/pull/37 # https://github.com/bigbluebutton/bbb-webrtc-sfu/pull/37
# yq w -i $TARGET kurento[0].url "ws://$SERVER_URL:8888/kurento" # yq w -i $TARGET kurento[0].url "ws://$SERVER_URL:8888/kurento"
# Set mediasoup IPs
yq w -i $TARGET mediasoup.webrtc.listenIps[0].announcedIp "$IP"
yq w -i $TARGET mediasoup.plainRtp.listenIp.announcedIp "$IP"
FREESWITCH_IP=$(xmlstarlet sel -t -v '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "local_ip_v4=")]/@data' /opt/freeswitch/conf/vars.xml | sed 's/local_ip_v4=//g') FREESWITCH_IP=$(xmlstarlet sel -t -v '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "local_ip_v4=")]/@data' /opt/freeswitch/conf/vars.xml | sed 's/local_ip_v4=//g')
if [ "$FREESWITCH_IP" != "" ]; then if [ "$FREESWITCH_IP" != "" ]; then
yq w -i $TARGET freeswitch.ip $FREESWITCH_IP yq w -i $TARGET freeswitch.ip $FREESWITCH_IP
@ -35,8 +39,8 @@ case "$1" in
# there's a problem rebuilding bufferutil # there's a problem rebuilding bufferutil
# do not abort in case npm rebuild return something different than 0 # do not abort in case npm rebuild return something different than 0
npm config set unsafe-perm true #npm config set unsafe-perm true
npm rebuild || true #npm rebuild || true
mkdir -p /var/log/bbb-webrtc-sfu/ mkdir -p /var/log/bbb-webrtc-sfu/
touch /var/log/bbb-webrtc-sfu/bbb-webrtc-sfu.log touch /var/log/bbb-webrtc-sfu/bbb-webrtc-sfu.log
@ -77,6 +81,10 @@ case "$1" in
# echo "#" # echo "#"
# fi # fi
# Creates the mediasoup raw media file dir if needed
if [ ! -d /var/mediasoup ]; then
mkdir -p /var/mediasoup
fi
# Create a symbolic link from /var/kurento -> /var/lib/kurento if needed # Create a symbolic link from /var/kurento -> /var/lib/kurento if needed
if [ ! -d /var/kurento ]; then if [ ! -d /var/kurento ]; then

View File

@ -15,6 +15,9 @@ case "$1" in
if [ -f /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml ]; then if [ -f /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml ]; then
cp /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml /tmp/bbb-webrtc-sfu-default.yml cp /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml /tmp/bbb-webrtc-sfu-default.yml
fi fi
# there might be remaining files from older BBB versions
# BBB 2.3 and earlier did an npm rebuild in the after-install script.
rm -rf /usr/local/bigbluebutton/bbb-webrtc-sfu/node_modules
;; ;;
abort-upgrade) abort-upgrade)

View File

@ -40,6 +40,14 @@ else
npm install --unsafe-perm --production npm install --unsafe-perm --production
fi fi
# clean out stuff that is not required in the final package
rm -rf node_modules/mediasoup/{rust,.github,test}
rm -rf node_modules/mediasoup/worker/{deps,src,test,include,fuzzer}
rm -rf node_modules/mediasoup/worker/out/Release/*.a
rm -rf node_modules/mediasoup/worker/out/Release/.deps
rm -rf node_modules/mediasoup/worker/out/Release/obj.target
rm -rf node_modules/mediasoup/worker/out/deps
popd popd
cp webrtc-sfu.nginx staging/etc/bigbluebutton/nginx cp webrtc-sfu.nginx staging/etc/bigbluebutton/nginx