Merge branch 'develop' of https://github.com/bigbluebutton/bigbluebutton into add-upload-toast

This commit is contained in:
KDSBrowne 2020-04-01 22:55:52 +00:00
commit 0bd6687387
60 changed files with 1216 additions and 197 deletions

View File

@ -26,6 +26,12 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**BBB version (optional):**
BigBlueButton continually evolves. Providing the version/build helps us to pinpoint when an issue was introduced.
Example:
$ sudo bbb-conf --check | grep BigBlueButton
BigBlueButton Server 2.2.2 (1816)
**Desktop (please complete the following information):**
- OS: [e.g. Windows, Mac]
- Browser [e.g. Chrome, Safari]

View File

@ -25,6 +25,9 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-bbb-alert:before {
content: "\e958";
}
.icon-bbb-mute:before {
content: "\e932";
}

View File

@ -81,6 +81,16 @@ class Base extends Component {
if (animations) HTML.classList.add('animationsEnabled');
if (!animations) HTML.classList.add('animationsDisabled');
if (getFromUserSettings('bbb_show_participants_on_login', true) && !deviceInfo.type().isPhone) {
Session.set('openPanel', 'userlist');
if (CHAT_ENABLED) {
Session.set('openPanel', 'chat');
Session.set('idChatOpen', PUBLIC_CHAT_ID);
}
} else {
Session.set('openPanel', '');
}
fullscreenChangedEvents.forEach((event) => {
document.addEventListener(event, Base.handleFullscreenChange);
});
@ -354,16 +364,6 @@ const BaseContainer = withTracker(() => {
});
}
if (getFromUserSettings('bbb_show_participants_on_login', true) && !deviceInfo.type().isPhone) {
Session.set('openPanel', 'userlist');
if (CHAT_ENABLED) {
Session.set('openPanel', 'chat');
Session.set('idChatOpen', PUBLIC_CHAT_ID);
}
} else {
Session.set('openPanel', '');
}
return {
approved,
ejected,

View File

@ -128,6 +128,7 @@ class ActionsDropdown extends PureComponent {
? (
<DropdownListItem
icon="polling"
data-test="polling"
label={formatMessage(pollBtnLabel)}
description={formatMessage(pollBtnDesc)}
key={this.pollId}
@ -246,7 +247,6 @@ class ActionsDropdown extends PureComponent {
<Button
hideLabel
aria-label={intl.formatMessage(intlMessages.actionsLabel)}
className={styles.button}
label={intl.formatMessage(intlMessages.actionsLabel)}
icon="plus"
color="primary"

View File

@ -24,7 +24,7 @@ const intlMessages = defineMessages({
const CaptionsButton = ({ intl, isActive, handleOnClick }) => (
<Button
className={cx(styles.button, isActive || styles.btn)}
className={cx(isActive || styles.btn)}
icon="closed_caption"
label={intl.formatMessage(isActive ? intlMessages.stop : intlMessages.start)}
color={isActive ? 'primary' : 'default'}

View File

@ -3,7 +3,6 @@ import cx from 'classnames';
import { styles } from './styles.scss';
import DesktopShare from './desktop-share/component';
import ActionsDropdown from './actions-dropdown/container';
import QuickPollDropdown from './quick-poll-dropdown/component';
import AudioControlsContainer from '../audio/audio-controls/container';
import JoinVideoOptionsContainer from '../video-provider/video-button/container';
import CaptionsButtonContainer from '/imports/ui/components/actions-bar/captions/container';
@ -57,18 +56,6 @@ class ActionsBar extends PureComponent {
isMeteorConnected,
}}
/>
{isPollingEnabled
? (
<QuickPollDropdown
{...{
currentSlidHasContent,
intl,
amIPresenter,
parseCurrentSlideContent,
}}
/>
) : null
}
{isCaptionsAvailable
? (
<CaptionsButtonContainer {...{ intl }} />

View File

@ -161,7 +161,7 @@ const DesktopShare = ({
return (shouldAllowScreensharing
? (
<Button
className={cx(styles.button, isVideoBroadcasting || styles.btn)}
className={cx(isVideoBroadcasting || styles.btn)}
disabled={(!isMeteorConnected && !isVideoBroadcasting) || !screenshareDataSavingSetting}
icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'}
label={intl.formatMessage(vLabel)}

View File

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import MediaService from '/imports/ui/components/media/service';
import { styles } from '../styles';
const propTypes = {
intl: intlShape.isRequired,
@ -27,7 +26,6 @@ const PresentationOptionsContainer = ({ intl, toggleSwapLayout, isThereCurrentPr
if (shouldUnswapLayout()) toggleSwapLayout();
return (
<Button
className={styles.button}
icon="presentation"
label={intl.formatMessage(intlMessages.restorePresentationLabel)}
description={intl.formatMessage(intlMessages.restorePresentationDesc)}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, intlShape } from 'react-intl';
import _ from 'lodash';
@ -44,6 +44,7 @@ const handleClickQuickPoll = (slideId, poll) => {
const { type } = poll;
Session.set('openPanel', 'poll');
Session.set('forcePollOpen', true);
Session.set('pollInitiated', true);
makeCall('startPoll', type, slideId);
};
@ -87,40 +88,92 @@ const getAvailableQuickPolls = (slideId, parsedSlides) => {
});
};
const QuickPollDropdown = (props) => {
const { amIPresenter, intl, parseCurrentSlideContent } = props;
const parsedSlide = parseCurrentSlideContent(
intl.formatMessage(intlMessages.yesOptionLabel),
intl.formatMessage(intlMessages.noOptionLabel),
intl.formatMessage(intlMessages.trueOptionLabel),
intl.formatMessage(intlMessages.falseOptionLabel),
);
class QuickPollDropdown extends Component {
render() {
const {
amIPresenter,
intl,
parseCurrentSlideContent,
startPoll,
currentSlide,
activePoll,
className,
} = this.props;
const { slideId, quickPollOptions } = parsedSlide;
const parsedSlide = parseCurrentSlideContent(
intl.formatMessage(intlMessages.yesOptionLabel),
intl.formatMessage(intlMessages.noOptionLabel),
intl.formatMessage(intlMessages.trueOptionLabel),
intl.formatMessage(intlMessages.falseOptionLabel),
);
return amIPresenter && quickPollOptions && quickPollOptions.length ? (
<Dropdown>
<DropdownTrigger tabIndex={0}>
const { slideId, quickPollOptions } = parsedSlide;
const quickPolls = getAvailableQuickPolls(slideId, quickPollOptions);
if (quickPollOptions.length === 0) return null;
let quickPollLabel = '';
if (quickPolls.length > 0) {
const { props: pollProps } = quickPolls[0];
quickPollLabel = pollProps.label;
}
let singlePollType = null;
if (quickPolls.length === 1 && quickPollOptions.length) {
const { type } = quickPollOptions[0];
singlePollType = type;
}
let btn = (
<Button
aria-label={intl.formatMessage(intlMessages.quickPollLabel)}
className={styles.quickPollBtn}
label={quickPollLabel}
tooltipLabel={intl.formatMessage(intlMessages.quickPollLabel)}
onClick={() => startPoll(singlePollType, currentSlide.id)}
size="lg"
disabled={!!activePoll}
/>
);
const usePollDropdown = quickPollOptions && quickPollOptions.length && quickPolls.length > 1;
let dropdown = null;
if (usePollDropdown) {
btn = (
<Button
aria-label={intl.formatMessage(intlMessages.quickPollLabel)}
circle
className={styles.button}
color="primary"
hideLabel
icon="polling"
label={intl.formatMessage(intlMessages.quickPollLabel)}
className={styles.quickPollBtn}
label={quickPollLabel}
tooltipLabel={intl.formatMessage(intlMessages.quickPollLabel)}
onClick={() => null}
size="lg"
disabled={!!activePoll}
/>
</DropdownTrigger>
<DropdownContent placement="top left">
<DropdownList>
{getAvailableQuickPolls(slideId, quickPollOptions)}
</DropdownList>
</DropdownContent>
</Dropdown>
) : null;
};
);
dropdown = (
<Dropdown className={className}>
<DropdownTrigger tabIndex={0}>
{btn}
</DropdownTrigger>
<DropdownContent placement="top left">
<DropdownList>
{quickPolls}
</DropdownList>
</DropdownContent>
</Dropdown>
);
}
return amIPresenter && usePollDropdown ? (
dropdown
) : (
btn
);
}
}
QuickPollDropdown.propTypes = propTypes;

View File

@ -0,0 +1,11 @@
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import { injectIntl } from 'react-intl';
import QuickPollDropdown from './component';
const QuickPollDropdownContainer = props => <QuickPollDropdown {...props} />;
export default withTracker(() => ({
activePoll: Session.get('pollInitiated') || false,
}))(injectIntl(QuickPollDropdownContainer));

View File

@ -73,9 +73,24 @@
}
}
.button {
.quickPollBtn {
padding: var(--whiteboard-toolbar-padding);
background-color: var(--color-off-white) !important;
box-shadow: none !important;
span:first-child {
box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
border: 1px solid var(--toolbar-button-color);
border-radius: var(--border-size-large);
color: var(--toolbar-button-color);
font-size: small;
font-weight: var(--headings-font-weight);
opacity: 1;
padding-right: var(--border-size-large);
padding-left: var(--border-size-large);
}
span:first-child:hover {
opacity: 1 !important;
}
}

View File

@ -25,6 +25,7 @@
.verticalList {
@extend %list;
flex-direction: column;
width: 100%;
}
.horizontalList {

View File

@ -97,6 +97,7 @@ class NavBar extends PureComponent {
ghost
circle
hideLabel
data-test={hasUnreadMessages ? 'hasUnreadMessages' : null}
label={intl.formatMessage(intlMessages.toggleUserListLabel)}
aria-label={ariaLabel}
icon="user"

View File

@ -228,6 +228,7 @@ class SettingsDropdown extends PureComponent {
(<DropdownListItem
key="list-item-settings"
icon="settings"
data-test="settings"
label={intl.formatMessage(intlMessages.settingsLabel)}
description={intl.formatMessage(intlMessages.settingsDesc)}
onClick={() => mountModal(<SettingsMenuContainer />)}

View File

@ -72,6 +72,7 @@ class TalkingIndicator extends PureComponent {
? `${intl.formatMessage(intlMessages.muteLabel)} ${callerName}`
: null
}
data-test={talking ? 'isTalking' : 'wasTalking'}
aria-label={ariaLabel}
aria-describedby={talking ? 'description' : null}
color="primary"

View File

@ -174,6 +174,7 @@ class Poll extends Component {
label={label}
color="default"
className={styles.pollBtn}
data-test="pollBtn"
key={_.uniqueId('quick-poll-')}
onClick={() => {
Session.set('pollInitiated', true);
@ -338,6 +339,7 @@ class Poll extends Component {
<header className={styles.header}>
<Button
ref={(node) => { this.hideBtn = node; }}
data-test="hidePollDesc"
tabIndex={0}
label={intl.formatMessage(intlMessages.pollPaneTitle)}
icon="left_arrow"
@ -357,6 +359,7 @@ class Poll extends Component {
}
Session.set('openPanel', 'userlist');
Session.set('forcePollOpen', false);
Session.set('pollInitiated', false);
}}
className={styles.closeBtn}
icon="close"

View File

@ -190,6 +190,7 @@ class LiveResult extends PureComponent {
<Button
disabled={!isMeteorConnected}
onClick={() => {
Session.set('pollInitiated', false);
Service.publishPoll();
const { answers, numRespondents } = currentPoll;
@ -205,6 +206,7 @@ class LiveResult extends PureComponent {
stopPoll();
}}
label={intl.formatMessage(intlMessages.publishLabel)}
data-test="publishLabel"
color="primary"
className={styles.btn}
/>

View File

@ -528,6 +528,7 @@ class PresentationArea extends PureComponent {
fitToWidth,
zoom,
podId,
currentSlide,
}}
isFullscreen={isFullscreen}
fullscreenRef={this.refPresentationContainer}

View File

@ -10,6 +10,7 @@ import { styles } from './styles.scss';
import ZoomTool from './zoom-tool/component';
import FullscreenButtonContainer from '../../fullscreen-button/container';
import Tooltip from '/imports/ui/components/tooltip/component';
import QuickPollDropdownContainer from '/imports/ui/components/actions-bar/quick-poll-dropdown/container';
import KEY_CODES from '/imports/utils/keyCodes';
const intlMessages = defineMessages({
@ -211,6 +212,12 @@ class PresentationToolbar extends PureComponent {
isFullscreen,
fullscreenRef,
isMeteorConnected,
isPollingEnabled,
amIPresenter,
currentSlidHasContent,
parseCurrentSlideContent,
startPoll,
currentSlide,
} = this.props;
const BROWSER_RESULTS = browser();
@ -231,7 +238,25 @@ class PresentationToolbar extends PureComponent {
return (
<div id="presentationToolbarWrapper" className={styles.presentationToolbarWrapper}>
{this.renderAriaDescs()}
{<div />}
{
<div>
{isPollingEnabled
? (
<QuickPollDropdownContainer
{...{
currentSlidHasContent,
intl,
amIPresenter,
parseCurrentSlideContent,
startPoll,
currentSlide,
}}
className={styles.presentationBtn}
/>
) : null
}
</div>
}
{
<div className={styles.presentationSlideControls}>
<Button

View File

@ -3,9 +3,13 @@ import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import PresentationService from '/imports/ui/components/presentation/service';
import MediaService from '/imports/ui/components/media/service';
import Service from '/imports/ui/components/actions-bar/service';
import { makeCall } from '/imports/ui/services/api';
import PresentationToolbar from './component';
import PresentationToolbarService from './service';
const POLLING_ENABLED = Meteor.settings.public.poll.enabled;
const PresentationToolbarContainer = (props) => {
const {
userIsPresenter,
@ -30,7 +34,15 @@ export default withTracker((params) => {
presentationId,
} = params;
const startPoll = (type, id) => {
Session.set('openPanel', 'poll');
Session.set('forcePollOpen', true);
makeCall('startPoll', type, id);
};
return {
amIPresenter: Service.amIPresenter(),
layoutSwapped: MediaService.getSwapLayout() && MediaService.shouldEnableSwapLayout(),
userIsPresenter: PresentationService.isPresenter(podId),
numberOfSlides: PresentationToolbarService.getNumberOfSlides(podId, presentationId),
@ -38,6 +50,10 @@ export default withTracker((params) => {
previousSlide: PresentationToolbarService.previousSlide,
skipToSlide: PresentationToolbarService.skipToSlide,
isMeteorConnected: Meteor.status().connected,
isPollingEnabled: POLLING_ENABLED,
currentSlidHasContent: PresentationService.currentSlidHasContent(),
parseCurrentSlideContent: PresentationService.parseCurrentSlideContent,
startPoll,
};
})(PresentationToolbarContainer);

View File

@ -26,6 +26,16 @@
.presentationBtn {
position: relative;
color: var(--toolbar-button-color);
background-color: var(--color-off-white);
border-radius: 0;
box-shadow: none !important;
border: 0;
&:focus {
background-color: var(--color-off-white);
border: 0;
}
}
.presentationZoomControls {
@ -86,21 +96,6 @@
}
}
button,
select,
>div {
color: var(--toolbar-button-color);
background-color: var(--color-off-white);
border-radius: 0;
box-shadow: none !important;
border: 0;
&:focus {
background-color: var(--color-off-white);
border: 0;
}
}
i {
color: var(--toolbar-button-color);
display: flex;

View File

@ -846,7 +846,7 @@ class PresentationUploader extends Component {
disablePreview
onDrop={this.handleFiledrop}
>
<Icon className={styles.dropzoneIcon} iconName="upload" />
<Icon className={styles.dropzoneIcon} data-test="fileUploadDropZone" iconName="upload" />
<p className={styles.dropzoneMessage}>
{intl.formatMessage(intlMessages.dropzoneImagesLabel)}
&nbsp;

View File

@ -54,7 +54,7 @@
.presentationToolbar{
display: flex;
overflow-x: auto;
overflow-x: visible;
order: 2;
position: absolute;
bottom: 0;

View File

@ -6,6 +6,7 @@ import {
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import DataSaving from '/imports/ui/components/settings/submenus/data-saving/component';
import Application from '/imports/ui/components/settings/submenus/application/component';
import Notification from '/imports/ui/components/settings/submenus/notification/component';
import _ from 'lodash';
import PropTypes from 'prop-types';
@ -159,6 +160,14 @@ class Settings extends Component {
{/* <Icon iconName='video' className={styles.icon}/> */}
{/* <span id="videoTab">{intl.formatMessage(intlMessages.videoTabLabel)}</span> */}
{/* </Tab> */}
<Tab
className={styles.tabSelector}
// aria-labelledby="appTab"
selectedClassName={styles.selected}
>
<Icon iconName="alert" className={styles.icon} />
<span id="notificationTab">Notification</span>
</Tab>
<Tab
className={styles.tabSelector}
aria-labelledby="dataSavingTab"
@ -181,6 +190,12 @@ class Settings extends Component {
settings={current.application}
/>
</TabPanel>
<TabPanel className={styles.tabPanel}>
<Notification
handleUpdateSettings={this.handleUpdateSettings}
settings={current.application}
/>
</TabPanel>
{/* <TabPanel className={styles.tabPanel}> */}
{/* <Video */}
{/* handleUpdateSettings={this.handleUpdateSettings} */}

View File

@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import cx from 'classnames';
import Button from '/imports/ui/components/button/component';
import Toggle from '/imports/ui/components/switch/component';
@ -7,7 +7,6 @@ import BaseMenu from '../base/component';
import { styles } from '../styles';
const MIN_FONTSIZE = 0;
const CHAT_ENABLED = Meteor.settings.public.chat.enabled;
const intlMessages = defineMessages({
applicationSectionTitle: {
@ -18,22 +17,6 @@ const intlMessages = defineMessages({
id: 'app.submenu.application.animationsLabel',
description: 'animations label',
},
audioAlertLabel: {
id: 'app.submenu.application.audioAlertLabel',
description: 'audio notification label',
},
pushAlertLabel: {
id: 'app.submenu.application.pushAlertLabel',
description: 'push notifiation label',
},
userJoinAudioAlertLabel: {
id: 'app.submenu.application.userJoinAudioAlertLabel',
description: 'audio notification when a user joins',
},
userJoinPushAlertLabel: {
id: 'app.submenu.application.userJoinPushAlertLabel',
description: 'push notification when a user joins',
},
fontSizeControlLabel: {
id: 'app.submenu.application.fontSizeControlLabel',
description: 'label for font size ontrol',
@ -204,90 +187,6 @@ class ApplicationMenu extends BaseMenu {
</div>
</div>
{CHAT_ENABLED
? (<Fragment>
<div className={styles.row}>
<div className={styles.col} aria-hidden="true">
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.audioAlertLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentRight)}>
<Toggle
icons={false}
defaultChecked={this.state.settings.chatAudioAlerts}
onChange={() => this.handleToggle('chatAudioAlerts')}
ariaLabel={intl.formatMessage(intlMessages.audioAlertLabel)}
/>
</div>
</div>
</div>
<div className={styles.row}>
<div className={styles.col} aria-hidden="true">
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.pushAlertLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentRight)}>
<Toggle
icons={false}
defaultChecked={this.state.settings.chatPushAlerts}
onChange={() => this.handleToggle('chatPushAlerts')}
ariaLabel={intl.formatMessage(intlMessages.pushAlertLabel)}
/>
</div>
</div>
</div>
</Fragment>
) : null
}
<div className={styles.row}>
<div className={styles.col} aria-hidden="true">
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.userJoinAudioAlertLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentRight)}>
<Toggle
icons={false}
defaultChecked={this.state.settings.userJoinAudioAlerts}
onChange={() => this.handleToggle('userJoinAudioAlerts')}
ariaLabel={intl.formatMessage(intlMessages.userJoinAudioAlertLabel)}
/>
</div>
</div>
</div>
<div className={styles.row}>
<div className={styles.col} aria-hidden="true">
<div className={styles.formElement}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.userJoinPushAlertLabel)}
</label>
</div>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentRight)}>
<Toggle
icons={false}
defaultChecked={this.state.settings.userJoinPushAlerts}
onChange={() => this.handleToggle('userJoinPushAlerts')}
ariaLabel={intl.formatMessage(intlMessages.userJoinPushAlertLabel)}
/>
</div>
</div>
</div>
<div className={styles.row}>
<div className={styles.col} aria-hidden="true">
<div className={styles.formElement}>

View File

@ -0,0 +1,167 @@
import React from 'react';
import cx from 'classnames';
import Toggle from '/imports/ui/components/switch/component';
import { defineMessages, injectIntl } from 'react-intl';
import BaseMenu from '../base/component';
import { styles } from '../styles';
const CHAT_ENABLED = Meteor.settings.public.chat.enabled;
const intlMessages = defineMessages({
notificationSectionTitle: {
id: 'app.submenu.notification.SectionTitle',
description: 'Notification section title',
},
notificationSectionDesc: {
id: 'app.submenu.notification.Desc',
description: 'provides extra info for notification section',
},
audioAlertLabel: {
id: 'app.submenu.notification.audioAlertLabel',
description: 'audio notification label',
},
pushAlertLabel: {
id: 'app.submenu.notification.pushAlertLabel',
description: 'push notifiation label',
},
messagesLabel: {
id: 'app.submenu.notification.messagesLabel',
description: 'label for chat messages',
},
userJoinLabel: {
id: 'app.submenu.notification.userJoinLabel',
description: 'label for chat messages',
},
raiseHandLabel: {
id: 'app.submenu.notification.raiseHandLabel',
description: 'label for raise hand emoji notifications',
},
});
class NotificationMenu extends BaseMenu {
constructor(props) {
super(props);
this.state = {
settingsName: 'notification',
settings: props.settings,
};
}
render() {
const { intl } = this.props;
const { settings } = this.state;
return (
<div>
<div>
<h3 className={styles.title}>
{intl.formatMessage(intlMessages.notificationSectionTitle)}
</h3>
<h4 className={styles.subtitle}>{intl.formatMessage(intlMessages.notificationSectionDesc)}</h4>
</div>
<div className={styles.form}>
<div className={styles.row}>
<div className={styles.col} />
<div className={cx(styles.col, styles.colHeading)}>
{intl.formatMessage(intlMessages.audioAlertLabel)}
</div>
<div className={cx(styles.col, styles.colHeading)}>
{intl.formatMessage(intlMessages.pushAlertLabel)}
</div>
</div>
{CHAT_ENABLED ? (
<div className={styles.row}>
<div className={styles.col}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.messagesLabel)}
</label>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentCenter)}>
<Toggle
icons={false}
defaultChecked={settings.chatAudioAlerts}
onChange={() => this.handleToggle('chatAudioAlerts')}
ariaLabel={`${intl.formatMessage(intlMessages.messagesLabel)} ${intl.formatMessage(intlMessages.audioAlertLabel)}`}
/>
</div>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentCenter)}>
<Toggle
icons={false}
defaultChecked={settings.chatPushAlerts}
onChange={() => this.handleToggle('chatPushAlerts')}
ariaLabel={`${intl.formatMessage(intlMessages.messagesLabel)} ${intl.formatMessage(intlMessages.pushAlertLabel)}`}
/>
</div>
</div>
</div>) : null
}
<div className={styles.row}>
<div className={styles.col}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.userJoinLabel)}
</label>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentCenter)}>
<Toggle
icons={false}
defaultChecked={settings.userJoinAudioAlerts}
onChange={() => this.handleToggle('userJoinAudioAlerts')}
ariaLabel={`${intl.formatMessage(intlMessages.userJoinLabel)} ${intl.formatMessage(intlMessages.audioAlertLabel)}`}
/>
</div>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentCenter)}>
<Toggle
icons={false}
defaultChecked={settings.userJoinPushAlerts}
onChange={() => this.handleToggle('userJoinPushAlerts')}
ariaLabel={`${intl.formatMessage(intlMessages.userJoinLabel)} ${intl.formatMessage(intlMessages.pushAlertLabel)}`}
/>
</div>
</div>
</div>
<div className={styles.row}>
<div className={styles.col}>
<label className={styles.label}>
{intl.formatMessage(intlMessages.raiseHandLabel)}
</label>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentCenter)}>
<Toggle
icons={false}
defaultChecked={settings.raiseHandAudioAlerts}
onChange={() => this.handleToggle('raiseHandAudioAlerts')}
ariaLabel={`${intl.formatMessage(intlMessages.raiseHandLabel)} ${intl.formatMessage(intlMessages.audioAlertLabel)}`}
/>
</div>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentCenter)}>
<Toggle
icons={false}
defaultChecked={settings.raiseHandPushAlerts}
onChange={() => this.handleToggle('raiseHandPushAlerts')}
ariaLabel={`${intl.formatMessage(intlMessages.raiseHandLabel)} ${intl.formatMessage(intlMessages.pushAlertLabel)}`}
/>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default injectIntl(NotificationMenu);

View File

@ -44,6 +44,14 @@
}
}
.colHeading {
display: block;
text-align: center;
font-size: 0.9rem;
margin-bottom: 0.5rem;
font-weight: bold;
}
.label {
color: var(--color-gray-label);
font-size: 0.9rem;

View File

@ -37,7 +37,7 @@ const Toast = ({
<div className={cx(styles.icon, small ? styles.smallIcon : null)}>
<Icon iconName={icon || defaultIcons[type]} />
</div>
<div className={cx(styles.message, small ? styles.smallMessage : null)}>
<div data-test="toastSmallMsg" className={cx(styles.message, small ? styles.smallMessage : null)}>
<span>{message}</span>
</div>
</div>

View File

@ -114,6 +114,7 @@
.presenter {
&:before {
content: "\00a0\e90b\00a0";
padding: var(--md-padding-y);
}
@include presenterIndicator();
}

View File

@ -45,6 +45,9 @@ class UserListItem extends PureComponent {
isMeteorConnected,
isMe,
voiceUser,
notify,
raiseHandAudioAlert,
raiseHandPushAlert,
} = this.props;
const contents = (
@ -76,6 +79,9 @@ class UserListItem extends PureComponent {
isMeteorConnected,
isMe,
voiceUser,
notify,
raiseHandAudioAlert,
raiseHandPushAlert,
}}
/>
);

View File

@ -3,8 +3,10 @@ import { withTracker } from 'meteor/react-meteor-data';
import BreakoutService from '/imports/ui/components/breakout-room/service';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import Settings from '/imports/ui/services/settings';
import UserListItem from './component';
import UserListService from '/imports/ui/components/user-list/service';
import { notify } from '/imports/ui/services/notification';
const UserListItemContainer = props => <UserListItem {...props} />;
const isMe = intId => intId === Auth.userID;
@ -14,6 +16,7 @@ export default withTracker(({ user }) => {
const breakoutSequence = (findUserInBreakout || {}).sequence;
const Meeting = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { lockSettingsProps: 1 } });
const AppSettings = Settings.application;
return {
user,
@ -35,5 +38,8 @@ export default withTracker(({ user }) => {
getEmojiList: UserListService.getEmojiList(),
getEmoji: UserListService.getEmoji(),
hasPrivateChatBetweenUsers: UserListService.hasPrivateChatBetweenUsers,
notify,
raiseHandAudioAlert: AppSettings.raiseHandAudioAlerts,
raiseHandPushAlert: AppSettings.raiseHandPushAlerts,
};
})(UserListItemContainer);

View File

@ -99,6 +99,10 @@ const messages = defineMessages({
id: 'app.userList.menu.directoryLookup.label',
description: 'Directory lookup',
},
handAlertLabel: {
id: 'app.userList.handAlert',
description: 'text displayed in raise hand toast',
},
});
const propTypes = {
@ -114,6 +118,7 @@ const propTypes = {
};
const CHAT_ENABLED = Meteor.settings.public.chat.enabled;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const MAX_ALERT_RANGE = 550;
class UserDropdown extends PureComponent {
/**
@ -138,6 +143,8 @@ class UserDropdown extends PureComponent {
showNestedOptions: false,
};
this.audio = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename}/resources/sounds/bbb-handRaise.mp3`);
this.handleScroll = this.handleScroll.bind(this);
this.onActionsShow = this.onActionsShow.bind(this);
this.onActionsHide = this.onActionsHide.bind(this);
@ -480,12 +487,17 @@ class UserDropdown extends PureComponent {
renderUserAvatar() {
const {
intl,
normalizeEmojiName,
user,
currentUser,
userInBreakout,
breakoutSequence,
meetingIsBreakout,
voiceUser,
notify,
raiseHandAudioAlert,
raiseHandPushAlert,
} = this.props;
const { clientType } = user;
@ -497,6 +509,18 @@ class UserDropdown extends PureComponent {
const iconVoiceOnlyUser = (<Icon iconName="audio_on" />);
const userIcon = isVoiceOnly ? iconVoiceOnlyUser : iconUser;
const shouldAlert = user.emoji === 'raiseHand'
&& currentUser.userId !== user.userId
&& new Date() - user.emojiTime < MAX_ALERT_RANGE;
if (shouldAlert) {
if (raiseHandAudioAlert) this.audio.play();
if (raiseHandPushAlert) {
notify(
`${user.name} ${intl.formatMessage(messages.handAlertLabel)}`, 'info', 'hand',
);
}
}
return (
<UserAvatar

View File

@ -124,6 +124,7 @@ class VideoListItem extends Component {
return (
<FullscreenButtonContainer
data-test="presentationFullscreenButton"
fullscreenRef={this.videoContainer}
elementName={name}
isFullscreen={isFullscreen}
@ -158,7 +159,7 @@ class VideoListItem extends Component {
>
{
!videoIsReady
&& <div className={styles.connecting} />
&& <div data-test="webcamConnecting" className={styles.connecting} />
}
<div
className={styles.videoContainer}

View File

@ -10,7 +10,7 @@ public:
clientTitle: BigBlueButton
appName: BigBlueButton HTML5 Client
bbbServerVersion: 2.2-dev
copyright: "©2019 BigBlueButton Inc."
copyright: "©2020 BigBlueButton Inc."
html5ClientBuild: HTML5_CLIENT_VERSION
helpLink: https://bigbluebutton.org/html5/
lockOnJoin: true
@ -33,6 +33,8 @@ public:
chatPushAlerts: false
userJoinAudioAlerts: false
userJoinPushAlerts: false
raiseHandAudioAlerts: false
raiseHandPushAlerts: false
fallbackLocale: en
overrideLocale: null
audio:

View File

@ -60,6 +60,7 @@
"app.userList.messagesTitle": "Messages",
"app.userList.notesTitle": "Notes",
"app.userList.notesListItem.unreadContent": "New content is available in the shared notes section",
"app.userList.handAlert": "has raised their hand",
"app.userList.captionsTitle": "Captions",
"app.userList.presenter": "Presenter",
"app.userList.you": "You",
@ -290,10 +291,6 @@
"app.screenshare.screenShareLabel" : "Screen share",
"app.submenu.application.applicationSectionTitle": "Application",
"app.submenu.application.animationsLabel": "Animations",
"app.submenu.application.audioAlertLabel": "Audio Alerts for Chat",
"app.submenu.application.pushAlertLabel": "Popup Alerts for Chat",
"app.submenu.application.userJoinAudioAlertLabel": "Audio Alerts for User Join",
"app.submenu.application.userJoinPushAlertLabel": "Popup Alerts for User Join",
"app.submenu.application.fontSizeControlLabel": "Font size",
"app.submenu.application.increaseFontBtnLabel": "Increase application font size",
"app.submenu.application.decreaseFontBtnLabel": "Decrease application font size",
@ -301,6 +298,12 @@
"app.submenu.application.languageLabel": "Application Language",
"app.submenu.application.languageOptionLabel": "Choose language",
"app.submenu.application.noLocaleOptionLabel": "No active locales",
"app.submenu.notification.SectionTitle": "Notifications",
"app.submenu.notification.Desc": "Define how and what you will be notified.",
"app.submenu.notification.audioAlertLabel": "Audio Alerts",
"app.submenu.notification.pushAlertLabel": "Popup Alerts",
"app.submenu.notification.messagesLabel": "Chat Message",
"app.submenu.notification.userJoinLabel": "User Join",
"app.submenu.audio.micSourceLabel": "Microphone source",
"app.submenu.audio.speakerSourceLabel": "Speaker source",
"app.submenu.audio.streamVolumeLabel": "Your audio stream volume",
@ -504,6 +507,7 @@
"app.notification.recordingPaused": "This session is not being recorded anymore",
"app.notification.recordingAriaLabel": "Recorded time ",
"app.notification.userJoinPushAlert": "{0} joined the session",
"app.submenu.notification.raiseHandLabel": "Raise hand",
"app.shortcut-help.title": "Keyboard shortcuts",
"app.shortcut-help.accessKeyNotAvailable": "Access keys not available",
"app.shortcut-help.comboLabel": "Combo",

View File

@ -1,2 +1,18 @@
# meeting credentials
BBB_SERVER_URL=""
BBB_SHARED_SECRET=""
BBB_SHARED_SECRET=""
# collecting metrics
BBB_COLLECT_METRICS= # (true/false): true to collect metrics
METRICS_FOLDER= # full path of your audio.wav file
# files paths for audio and webcams tests
AUDIO_FILE= # full path of your audio.wav file
VIDEO_FILE= # full path of your video.y4m file
# webcams test
LOOP_INTERVAL= # time to loop in the webcams test in milliseconds
CAMERA_SHARE_FAILED_WAIT_TIME=15000 # this is set by default in the BBB server
# audio test
IS_AUDIO_TEST= # (true/false): true if the test will require enabling audio

View File

@ -0,0 +1,31 @@
const Audio = require('./audio/audio');
describe('Audio', () => {
test('Join audio', async () => {
const test = new Audio();
let response;
try {
await test.init();
response = await test.test();
} catch (e) {
console.log(e);
} finally {
await test.close();
}
expect(response).toBe(true);
});
test('Mute the other User', async () => {
const test = new Audio();
let response;
try {
await test.init();
response = await test.mute();
} catch (e) {
console.log(e);
} finally {
await test.close();
}
expect(response).toBe(true);
});
});

View File

@ -0,0 +1,62 @@
const utilNotification = require('../notifications/util');
const Page = require('../core/page');
const params = require('../params');
const util = require('./util');
class Audio {
constructor() {
this.page1 = new Page();
this.page2 = new Page();
}
// Join BigBlueButton meeting
async init(meetingId) {
await this.page1.init(Page.getArgsWithAudio(), meetingId, { ...params, fullName: 'BroadCaster1' });
await this.page2.init(Page.getArgsWithAudio(), this.page1.meetingId, { ...params, fullName: 'BroadCaster2' });
await this.page1.joinMicrophone();
await this.page2.joinMicrophone();
}
async initOneUser(page,meetingId) {
await page.init(Page.getArgsWithAudio(), meetingId, {...params, fullName: 'User1'});
await page.joinMicrophone();
}
async test() {
// User1 is checking if User2 is talking
const isTalkingIndicatorUser1 = await util.checkUserIsTalkingIndicator(this.page1);
// User2 is checking if User1 is talking
const isTalkingIndicatorUser2 = await util.checkUserIsTalkingIndicator(this.page2);
const doneCheckingIsTalkingIndicator = isTalkingIndicatorUser1 && isTalkingIndicatorUser2;
const response = doneCheckingIsTalkingIndicator == true;
return response;
}
async mute() {
// User1 mutes User2 & User2 mutes User1
await util.mute(this.page1, this.page2);
// User1 checks if he still can see User2 highlighting
const wasTalkingIndicatorUser1 = await util.checkUserWasTalkingIndicator(this.page1);
// User2 checks if he still can see User1 highlighting
const wasTalkingIndicatorUser2 = await util.checkUserWasTalkingIndicator(this.page2);
const doneCheckingIsTalkingIndicator = wasTalkingIndicatorUser1 && wasTalkingIndicatorUser2;
const response = doneCheckingIsTalkingIndicator == true;
return response;
}
async audioNotification(page) {
const resp = await utilNotification.getLastToastValue(page);
return resp;
}
async close() {
this.page1.close();
this.page2.close();
}
}
module.exports = exports = Audio;

View File

@ -0,0 +1,41 @@
const pe = require('../core/elements');
const ule = require('../user/elements');
async function checkUserAvatarIfHighlighting(test) {
await test.waitForSelector(ule.statusIcon);
await test.waitForSelector('[class^="talking--"]');
const response = await test.page.evaluate(async () => await document.querySelectorAll('[data-test="userAvatar"]')[1].querySelectorAll('[class^="talking--"]') !== null);
return response;
}
async function checkUserIsTalkingIndicator(test) {
const response = await test.page.evaluate(getTestElement, pe.isTalking) !== null;
return response;
}
async function checkUserWasTalkingIndicator(test) {
const response = await test.page.evaluate(getTestElement, pe.wasTalking) !== null;
return response;
}
async function getTestElement(element) {
await document.querySelectorAll(element)[1];
}
async function clickTestElement(element) {
await document.querySelectorAll(element)[0].click();
}
async function mute(test) {
await test.page.evaluate(async () => {
await document.querySelectorAll('[data-test="userListItem"]')[0].click();
await document.querySelectorAll('[data-test="mute"]')[0].click();
});
}
exports.mute = mute;
exports.clickTestElement = clickTestElement;
exports.getTestElement = getTestElement;
exports.checkUserAvatarIfHighlighting = checkUserAvatarIfHighlighting;
exports.checkUserIsTalkingIndicator = checkUserIsTalkingIndicator;
exports.checkUserWasTalkingIndicator = checkUserWasTalkingIndicator;

View File

@ -1,4 +1,4 @@
exports.audioDialog = '.ReactModal__Content[aria-label="Join audio modal"]';
exports.audioDialog = '[aria-label="Join audio modal"]';
exports.closeAudio = 'button[aria-label="Close Join audio modal"]';
exports.microphoneButton = 'button[aria-label="Microphone"]';
exports.listenButton = 'button[aria-label="Listen Only"]';
@ -6,6 +6,11 @@ exports.echoYes = 'button[aria-label="Echo is audible"]';
exports.title = '._imports_ui_components_nav_bar__styles__presentationTitle';
exports.alerts = '.toastify-content';
exports.isTalking = '[data-test="isTalking"]';
exports.wasTalking = '[data-test="wasTalking"]';
exports.joinAudio = 'button[aria-label="Join Audio"]';
exports.leaveAudio = 'button[aria-label="Leave Audio"]';
exports.actions = 'button[aria-label="Actions"]';
exports.options = 'button[aria-label="Options"]';
exports.userList = 'button[aria-label="Users and Messages Toggle"]';
@ -13,3 +18,4 @@ exports.joinAudio = 'button[aria-label="Join Audio"]';
exports.leaveAudio = 'button[aria-label="Leave Audio"]';
exports.videoMenu = 'button[aria-label="Open video menu dropdown"]';
exports.screenShare = 'button[aria-label="Share your screen"]';
exports.screenShareVideo = '[id="screenshareVideo"]';

View File

@ -32,16 +32,38 @@ class Page {
const joinURL = helper.getJoinURL(this.meetingId, this.effectiveParams, isModerator);
await this.page.goto(joinURL);
await this.waitForSelector(e.audioDialog);
await this.click(e.closeAudio, true);
const checkForGetMetrics = async () => {
if (process.env.BBB_COLLECT_METRICS === 'true') {
await this.getMetrics();
}
};
if (process.env.IS_AUDIO_TEST !== 'true') {
await this.closeAudioModal();
}
await checkForGetMetrics();
}
// Joining audio with microphone
async joinMicrophone() {
await this.waitForSelector(e.audioDialog);
await this.waitForSelector(e.microphoneButton);
await this.click(e.microphoneButton, true);
await this.waitForSelector(e.echoYes);
await this.click(e.echoYes, true);
}
// Joining audio with Listen Only mode
async listenOnly() {
await this.waitForSelector(e.audioDialog);
await this.waitForSelector(e.listenButton);
await this.click(e.listenButton);
}
async closeAudioModal() {
await this.waitForSelector(e.audioDialog);
await this.click(e.closeAudio, true);
}
async setDownloadBehavior(downloadPath) {
const downloadBehavior = { behavior: 'allow', downloadPath };
await this.page._client.send('Page.setDownloadBehavior', downloadBehavior);
@ -65,6 +87,19 @@ class Page {
return { headless: false, args: ['--no-sandbox', '--use-fake-ui-for-media-stream'] };
}
static getArgsWithAudio() {
return {
headless: false,
args: [
'--no-sandbox',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
`--use-file-for-fake-audio-capture=${process.env.AUDIO_FILE}`,
'--allow-file-access',
],
};
}
static getArgsWithVideo() {
return {
headless: false,
@ -73,7 +108,7 @@ class Page {
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
`--use-file-for-fake-video-capture=${process.env.VIDEO_FILE}`,
'--allow-file-access'
'--allow-file-access',
],
};
}

View File

@ -0,0 +1,3 @@
exports.sharedNotes = 'div[data-test="sharedNotes"]';
exports.hideNoteLabel = 'button[data-test="hideNoteLabel"]';
exports.etherpad = 'iframe[title="etherpad"]';

View File

@ -0,0 +1,19 @@
const Create = require('../breakout/create');
const util = require('./util');
class SharedNotes extends Create {
constructor() {
super('shared-notes');
}
async test() {
const response = await util.startSharedNotes(this.page1);
return response;
}
async close() {
await this.page1.close();
await this.page2.close();
}
}
module.exports = exports = SharedNotes;

View File

@ -0,0 +1,18 @@
const se = require('./elements');
async function startSharedNotes(test) {
await test.waitForSelector(se.sharedNotes);
await test.click(se.sharedNotes, true);
await test.waitForSelector(se.hideNoteLabel);
const resp = await test.page.evaluate(getTestElement, se.etherpad);
await test.waitForSelector(se.etherpad);
return resp;
}
async function getTestElement(element) {
const response = document.querySelectorAll(element).length >= 1;
return response;
}
exports.getTestElement = getTestElement;
exports.startSharedNotes = startSharedNotes;

View File

@ -0,0 +1,122 @@
const Notifications = require('./notifications/notifications');
const ShareScreen = require('./screenshare/screenshare');
const Audio = require('./audio/audio');
describe('Notifications', () => {
test('Save settings notification', async () => {
const test = new Notifications();
let response;
try {
await test.init();
response = await test.saveSettingsNotification();
} catch (e) {
console.log(e);
} finally {
await test.close();
}
expect(response).toBe(true);
});
test('Public Chat notification', async () => {
const test = new Notifications();
let response;
try {
await test.init();
response = await test.publicChatNotification();
} catch (e) {
console.log(e);
} finally {
await test.close();
}
expect(response).toBe(true);
});
test('Private Chat notification', async () => {
const test = new Notifications();
let response;
try {
await test.init();
response = await test.privateChatNotification();
} catch (e) {
console.log(e);
} finally {
await test.close();
}
expect(response).toBe(true);
});
test('User join notification', async () => {
const test = new Notifications();
let response;
try {
await test.initUser3();
await test.userJoinNotification();
await test.initUser4();
response = await test.getUserJoinPopupResponse();
} catch (e) {
console.log(e);
} finally {
await test.closePages();
}
expect(response).toBe('User4 joined the session');
});
test('Presentation upload notification', async () => {
const test = new Notifications();
let response;
try {
await test.initUser3();
response = await test.fileUploaderNotification();
} catch (e) {
console.log(e);
} finally {
await test.closePage(test.page3);
}
expect(response).toContain('Current presentation');
});
test('Poll results notification', async () => {
const test = new Notifications();
let response;
try {
await test.initUser3();
response = await test.publishPollResults();
} catch (e) {
console.log(e);
} finally {
await test.closePage(test.page3);
}
expect(response).toContain('Poll results were published to Public Chat and Whiteboard');
});
test('Screenshare notification', async () => {
const test = new ShareScreen();
const page = new Notifications()
let response;
try {
await page.initUser3();
response = await test.toast(page.page3);
} catch (e) {
console.log(e);
} finally {
await page.closePage(page.page3);
}
expect(response).toBe('Screenshare has started');
});
test('Audio notifications', async () => {
const test = new Audio();
const page = new Notifications();
let response;
try {
process.env.IS_AUDIO_TEST = true;
await test.initOneUser(page.page3);
response = await test.audioNotification(page.page3);
} catch (e) {
console.log(e);
} finally {
await page.closePage(page.page3);
}
expect(response).toBe('You have joined the audio conference');
})
});

View File

@ -0,0 +1,21 @@
exports.settings = 'li[data-test="settings"]';
exports.settingsModal = 'div[aria-label="Settings"]';
exports.chatPushAlerts = '[data-test="chatPushAlerts"]';
exports.smallToastMsg = 'div[data-test="toastSmallMsg"]';
exports.saveSettings = '[data-test="modalConfirmButton"]';
exports.savedSettingsToast = 'Settings have been saved';
exports.publicChatToast = 'New Public Chat message';
exports.privateChatToast = 'New Private Chat message';
exports.userListNotifiedIcon = '[class^=btnWithNotificationDot]';
exports.hasUnreadMessages = 'button[data-test="hasUnreadMessages"]';
exports.modalConfirmButton = 'button[data-test="modalConfirmButton"]';
exports.userJoinPushAlerts = '[data-test="userJoinPushAlerts"]';
exports.uploadPresentation = '[data-test="uploadPresentation"]';
exports.dropdownContent = '[data-test="dropdownContent"]';
exports.fileUploadDropZone = '[data-test="fileUploadDropZone"]';
exports.polling = '[data-test="polling"]';
exports.hidePollDesc = '[data-test="hidePollDesc"]';
exports.pollBtn = '[data-test="pollBtn"]';
exports.publishLabel = '[data-test="publishLabel"]';

View File

@ -0,0 +1,101 @@
const MultiUsers = require('../user/multiusers');
const Page = require('../core/page');
const params = require('../params');
const util = require('./util');
const ne = require('./elements');
const we = require('../whiteboard/elements');
class Notifications extends MultiUsers {
constructor() {
super('notifications');
this.page1 = new Page();
this.page2 = new Page();
this.page3 = new Page();
this.page4 = new Page();
}
async init(meetingId) {
await this.page1.init(Page.getArgs(), meetingId, { ...params });
await this.page2.init(Page.getArgs(), this.page1.meetingId, { ...params, fullName: 'User2' });
}
async initUser3(meetingId) {
await this.page3.init(Page.getArgs(), meetingId, { ...params, fullName: 'User3' });
}
async initUser4() {
await this.page4.init(Page.getArgs(), this.page3.meetingId, { ...params, fullName: 'User4' });
}
// Save Settings toast notification
async saveSettingsNotification() {
await util.popupMenu(this.page1);
await util.saveSettings(this.page1);
const resp = await util.getLastToastValue(this.page1) === ne.savedSettingsToast;
return resp;
}
// Public chat toast notification
async publicChatNotification() {
await util.popupMenu(this.page1);
await util.enableChatPopup(this.page1);
await util.saveSettings(this.page1);
const expectedToastValue = await util.publicChatMessageToast(this.page1, this.page2);
await this.page1.waitForSelector(ne.smallToastMsg);
await this.page1.waitForSelector(ne.hasUnreadMessages);
const lastToast = await util.getOtherToastValue(this.page1);
return expectedToastValue === lastToast;
}
// Private chat toast notification
async privateChatNotification() {
await util.popupMenu(this.page1);
await util.enableChatPopup(this.page1);
await util.saveSettings(this.page1);
const expectedToastValue = await util.privateChatMessageToast(this.page2);
await this.page1.waitForSelector(ne.smallToastMsg);
await this.page1.waitForSelector(ne.hasUnreadMessages);
const lastToast = await util.getOtherToastValue(this.page1);
return expectedToastValue === lastToast;
}
// User join toast notification
async userJoinNotification() {
await util.popupMenu(this.page3);
await util.enableUserJoinPopup(this.page3);
await util.saveSettings(this.page3);
}
async getUserJoinPopupResponse() {
await this.page3.waitForSelector(ne.smallToastMsg);
const response = await util.getOtherToastValue(this.page3);
return response;
}
// File upload notification
async fileUploaderNotification() {
await util.uploadFileMenu(this.page3);
await this.page3.waitForSelector(ne.fileUploadDropZone);
const inputUploadHandle = await this.page3.page.$('input[type=file]');
await inputUploadHandle.uploadFile(process.env.PDF_FILE);
await this.page3.page.evaluate(util.clickTestElement, ne.modalConfirmButton);
const resp = await util.getLastToastValue(this.page3);
await this.page3.waitForSelector(we.whiteboard);
return resp;
}
async publishPollResults() {
await this.page3.waitForSelector(we.whiteboard);
await util.startPoll(this.page3);
await this.page3.waitForSelector(ne.smallToastMsg);
const resp = await util.getLastToastValue(this.page3);
return resp;
}
async closePages() {
await this.page3.close();
await this.page4.close();
}
}
module.exports = exports = Notifications;

View File

@ -0,0 +1,125 @@
const ne = require('../notifications/elements');
const ule = require('../user/elements');
const ce = require('../chat/elements');
const e = require('../core/elements');
async function clickTestElement(element) {
await document.querySelectorAll(element)[0].click();
}
async function popupMenu(page) {
await page.page.evaluate(clickTestElement, e.options);
await page.page.evaluate(clickTestElement, ne.settings);
}
async function enableChatPopup(test) {
await test.waitForSelector(ne.chatPushAlerts);
await test.page.evaluate(() => document.querySelector('[data-test="chatPushAlerts"]').children[0].click());
}
async function enableUserJoinPopup(test) {
await test.waitForSelector(ne.userJoinPushAlerts);
await test.page.evaluate(() => document.querySelector('[data-test="userJoinPushAlerts"]').children[0].click());
}
async function saveSettings(page) {
await page.waitForSelector(ne.saveSettings);
await page.click(ne.saveSettings, true);
}
async function waitForToast(test) {
await test.waitForSelector(ne.smallToastMsg);
const resp = await test.page.evaluate(getTestElement, ne.smallToastMsg) !== null;
return resp;
}
async function getLastToastValue(test) {
await test.waitForSelector(ne.smallToastMsg);
const toast = test.page.evaluate(() => {
const lastToast = document.querySelectorAll('[data-test="toastSmallMsg"]')[0].innerText;
return lastToast;
});
return toast;
}
async function getOtherToastValue(test) {
await test.waitForSelector(ne.smallToastMsg);
const toast = test.page.evaluate(() => {
const lastToast = document.querySelectorAll('[data-test="toastSmallMsg"]')[1].innerText;
return lastToast;
});
return toast;
}
async function getTestElement(element) {
await document.querySelectorAll(element)[1];
}
async function clickOnElement(element) {
await document.querySelectorAll(element)[0].click();
}
async function clickThePrivateChatButton(element) {
await document.querySelectorAll(element)[0].click();
}
async function publicChatMessageToast(page1, page2) {
// Open private Chat with the other User
await page1.page.evaluate(clickOnElement, ule.userListItem);
await page1.page.evaluate(clickThePrivateChatButton, ce.activeChat);
// send a public message
await page2.page.type(ce.publicChat, ce.publicMessage1);
await page2.page.click(ce.sendButton, true);
return ne.publicChatToast;
}
async function privateChatMessageToast(page2) {
// Open private Chat with the other User
await page2.page.evaluate(clickOnElement, ule.userListItem);
await page2.page.evaluate(clickThePrivateChatButton, ce.activeChat);
// send a private message
await page2.page.type(ce.privateChat, ce.message1);
await page2.page.click(ce.sendButton, true);
return ne.privateChatToast;
}
// File upload notification
async function uploadFileMenu(test) {
await test.page.evaluate(clickOnElement, ne.dropdownContent);
await test.page.evaluate(clickOnElement, ne.uploadPresentation);
}
async function getFileItemStatus(element, value) {
document.querySelectorAll(element)[1].innerText.includes(value);
}
async function clickRandomPollOption(element) {
document.querySelector(element).click();
}
async function startPoll(test) {
await test.page.evaluate(clickOnElement, ne.dropdownContent);
await test.page.evaluate(clickOnElement, ne.polling);
await test.waitForSelector(ne.hidePollDesc);
await test.waitForSelector(ne.pollBtn);
await test.page.evaluate(clickRandomPollOption, ne.pollBtn);
await test.waitForSelector(ne.publishLabel);
await test.page.evaluate(clickOnElement, ne.publishLabel);
await test.waitForSelector(ne.smallToastMsg);
}
exports.getFileItemStatus = getFileItemStatus;
exports.privateChatMessageToast = privateChatMessageToast;
exports.publicChatMessageToast = publicChatMessageToast;
exports.enableUserJoinPopup = enableUserJoinPopup;
exports.getOtherToastValue = getOtherToastValue;
exports.getLastToastValue = getLastToastValue;
exports.enableChatPopup = enableChatPopup;
exports.uploadFileMenu = uploadFileMenu;
exports.getTestElement = getTestElement;
exports.saveSettings = saveSettings;
exports.waitForToast = waitForToast;
exports.popupMenu = popupMenu;
exports.clickTestElement = clickTestElement;
exports.startPoll = startPoll;
exports.clickOnElement = clickOnElement;

View File

@ -0,0 +1,18 @@
const ShareScreen = require('./screenshare/screenshare');
const Page = require('./core/page');
describe('Screen Share', () => {
test('Share screen', async () => {
const test = new ShareScreen();
let response;
try {
await test.init(Page.getArgsWithVideo());
response = await test.test();
} catch (e) {
console.log(e);
} finally {
await test.close();
}
expect(response).toBe(true);
});
});

View File

@ -0,0 +1,26 @@
const Page = require('../core/page');
const utilNotifications = require('../notifications/util');
const util = require('./util');
const e = require('../core/elements');
class ShareScreen extends Page {
constructor() {
super('share-screen');
}
async test() {
await util.startScreenshare(this.page);
await this.page.waitForSelector(e.screenShareVideo);
const response = await util.getScreenShareContainer(this.page);
return response;
}
async toast(page) {
await util.startScreenshare(page);
const response = await utilNotifications.getLastToastValue(page);
return response;
}
}
module.exports = exports = ShareScreen;

View File

@ -0,0 +1,21 @@
const e = require('../core/elements');
async function startScreenshare(test) {
await test.waitForSelector(e.screenShare);
await test.click(e.screenShare, true);
}
async function getTestElement(element) {
(await document.querySelectorAll(element)[0]) !== null;
}
async function getScreenShareContainer(test) {
await test.waitForSelector(e.screenShareVideo);
const screenShareContainer = await test.evaluate(getTestElement, e.screenshareVideo);
const response = screenShareContainer !== null;
return response;
}
exports.getScreenShareContainer = getScreenShareContainer;
exports.getTestElement = getTestElement;
exports.startScreenshare = startScreenshare;

View File

@ -0,0 +1,17 @@
const SharedNotes = require('./notes/sharednotes');
describe('Shared notes', () => {
test('Open Shared notes', async () => {
const test = new SharedNotes();
let response;
try {
await test.init();
response = await test.test();
} catch (e) {
console.log(e);
} finally {
await test.close();
}
expect(response).toBe(true);
});
});

View File

@ -50,6 +50,10 @@ class MultiUsers {
await this.page1.close();
await this.page2.close();
}
async closePage(page) {
await page.close();
}
}
module.exports = exports = MultiUsers;

View File

@ -1,5 +1,6 @@
const Page = require('./core/page');
const Share = require('./webcam/share');
const Check = require('./webcam/check');
const Page = require('./core/page');
describe('Webcam', () => {
test('Shares webcam', async () => {
@ -15,4 +16,18 @@ describe('Webcam', () => {
}
expect(response).toBe(true);
});
test('Checks content of webcam', async () => {
const test = new Check();
let response;
try {
await test.init(Page.getArgsWithVideo());
response = await test.test();
} catch (e) {
console.log(e);
} finally {
await test.close();
}
expect(response).toBe(true);
});
});

View File

@ -0,0 +1,15 @@
const Share = require('./share');
const util = require('./util');
class Check extends Share {
constructor() {
super('check-webcam-content');
}
async test() {
await util.enableWebcam(this.page);
const respUser = await util.webcamContentCheck(this.page);
return respUser === true;
}
}
module.exports = exports = Check;

View File

@ -2,3 +2,4 @@ exports.joinVideo = 'button[data-test="joinVideo"]';
exports.videoPreview = 'video[data-test="videoPreview"]';
exports.startSharingWebcam = 'button[data-test="startSharingWebcam"]';
exports.videoContainer = 'video[data-test="videoContainer"]';
exports.webcamConnecting = '[data-test="webcamConnecting"]';

View File

@ -1,17 +1,14 @@
const Page = require('../core/page');
const util = require('./util');
const we = require('./elements');
class Share extends Page {
class Share extends Page{
constructor() {
super('share-webcam');
super('webcam-test');
}
async test() {
await util.enableWebcam(this.page);
await this.waitForSelector(we.videoContainer);
const videoContainer = await this.page.evaluate(util.getTestElement, we.videoContainer);
const response = videoContainer !== null;
const response = await util.evaluateCheck(this.page);
return response;
}
}

View File

@ -13,5 +13,58 @@ async function getTestElement(element) {
(await document.querySelectorAll(element)[0]) !== null;
}
async function evaluateCheck(test) {
await test.waitForSelector(we.videoContainer);
const videoContainer = await test.evaluate(getTestElement, we.presentationFullscreenButton);
const response = videoContainer !== null;
return response;
}
async function startAndCheckForWebcams(test) {
await enableWebcam(test);
const response = await evaluateCheck(test);
return response;
}
async function webcamContentCheck(test) {
await test.waitForSelector(we.videoContainer);
await test.waitForFunction(() => !document.querySelector('[data-test="webcamConnecting"]'));
const repeats = 5;
let check;
for (let i = repeats; i >= 1; i--) {
console.log(`loop ${i}`);
const checkCameras = function (i) {
const videos = document.querySelectorAll('video');
const lastVideoColor = document.lastVideoColor || {};
document.lastVideoColor = lastVideoColor;
for (let v = 0; v < videos.length; v++) {
const video = videos[v];
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
const pixel = context.getImageData(50, 50, 1, 1).data;
const pixelString = new Array(pixel).join(' ').toString();
if (lastVideoColor[v]) {
if (lastVideoColor[v] == pixelString) {
return false;
}
}
lastVideoColor[v] = pixelString;
return true;
}
};
check = await test.evaluate(checkCameras, i);
await test.waitFor(parseInt(process.env.LOOP_INTERVAL));
}
return check === true;
}
exports.startAndCheckForWebcams = startAndCheckForWebcams;
exports.webcamContentCheck = webcamContentCheck;
exports.evaluateCheck = evaluateCheck;
exports.getTestElement = getTestElement;
exports.enableWebcam = enableWebcam;