Merge branch 'develop' of https://github.com/bigbluebutton/bigbluebutton into add-upload-toast
This commit is contained in:
commit
0bd6687387
6
.github/ISSUE_TEMPLATE/html5-issue.md
vendored
6
.github/ISSUE_TEMPLATE/html5-issue.md
vendored
@ -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]
|
||||
|
@ -25,6 +25,9 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-bbb-alert:before {
|
||||
content: "\e958";
|
||||
}
|
||||
.icon-bbb-mute:before {
|
||||
content: "\e932";
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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'}
|
||||
|
@ -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 }} />
|
||||
|
@ -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)}
|
||||
|
@ -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)}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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));
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@
|
||||
.verticalList {
|
||||
@extend %list;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.horizontalList {
|
||||
|
@ -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"
|
||||
|
@ -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 />)}
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -528,6 +528,7 @@ class PresentationArea extends PureComponent {
|
||||
fitToWidth,
|
||||
zoom,
|
||||
podId,
|
||||
currentSlide,
|
||||
}}
|
||||
isFullscreen={isFullscreen}
|
||||
fullscreenRef={this.refPresentationContainer}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)}
|
||||
|
||||
|
@ -54,7 +54,7 @@
|
||||
|
||||
.presentationToolbar{
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-x: visible;
|
||||
order: 2;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
@ -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} */}
|
||||
|
@ -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}>
|
||||
|
@ -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);
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -114,6 +114,7 @@
|
||||
.presenter {
|
||||
&:before {
|
||||
content: "\00a0\e90b\00a0";
|
||||
padding: var(--md-padding-y);
|
||||
}
|
||||
@include presenterIndicator();
|
||||
}
|
||||
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
Binary file not shown.
BIN
bigbluebutton-html5/public/resources/sounds/bbb-handRaise.mp3
Normal file
BIN
bigbluebutton-html5/public/resources/sounds/bbb-handRaise.mp3
Normal file
Binary file not shown.
@ -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
|
31
bigbluebutton-html5/tests/puppeteer/audio.test.js
Normal file
31
bigbluebutton-html5/tests/puppeteer/audio.test.js
Normal 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);
|
||||
});
|
||||
});
|
62
bigbluebutton-html5/tests/puppeteer/audio/audio.js
Normal file
62
bigbluebutton-html5/tests/puppeteer/audio/audio.js
Normal 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;
|
41
bigbluebutton-html5/tests/puppeteer/audio/util.js
Normal file
41
bigbluebutton-html5/tests/puppeteer/audio/util.js
Normal 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;
|
@ -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"]';
|
||||
|
@ -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',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
3
bigbluebutton-html5/tests/puppeteer/notes/elements.js
Normal file
3
bigbluebutton-html5/tests/puppeteer/notes/elements.js
Normal file
@ -0,0 +1,3 @@
|
||||
exports.sharedNotes = 'div[data-test="sharedNotes"]';
|
||||
exports.hideNoteLabel = 'button[data-test="hideNoteLabel"]';
|
||||
exports.etherpad = 'iframe[title="etherpad"]';
|
19
bigbluebutton-html5/tests/puppeteer/notes/sharednotes.js
Normal file
19
bigbluebutton-html5/tests/puppeteer/notes/sharednotes.js
Normal 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;
|
18
bigbluebutton-html5/tests/puppeteer/notes/util.js
Normal file
18
bigbluebutton-html5/tests/puppeteer/notes/util.js
Normal 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;
|
122
bigbluebutton-html5/tests/puppeteer/notifications.test.js
Normal file
122
bigbluebutton-html5/tests/puppeteer/notifications.test.js
Normal 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');
|
||||
})
|
||||
});
|
@ -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"]';
|
@ -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;
|
125
bigbluebutton-html5/tests/puppeteer/notifications/util.js
Normal file
125
bigbluebutton-html5/tests/puppeteer/notifications/util.js
Normal 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;
|
18
bigbluebutton-html5/tests/puppeteer/screenshare.test.js
Normal file
18
bigbluebutton-html5/tests/puppeteer/screenshare.test.js
Normal 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);
|
||||
});
|
||||
});
|
@ -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;
|
21
bigbluebutton-html5/tests/puppeteer/screenshare/util.js
Normal file
21
bigbluebutton-html5/tests/puppeteer/screenshare/util.js
Normal 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;
|
17
bigbluebutton-html5/tests/puppeteer/sharednotes.test.js
Normal file
17
bigbluebutton-html5/tests/puppeteer/sharednotes.test.js
Normal 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);
|
||||
});
|
||||
});
|
@ -50,6 +50,10 @@ class MultiUsers {
|
||||
await this.page1.close();
|
||||
await this.page2.close();
|
||||
}
|
||||
|
||||
async closePage(page) {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = MultiUsers;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
15
bigbluebutton-html5/tests/puppeteer/webcam/check.js
Normal file
15
bigbluebutton-html5/tests/puppeteer/webcam/check.js
Normal 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;
|
@ -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"]';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user