From 4d942440dbf36eaec72729f5c5cdd32c10ee5172 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Wed, 7 Mar 2018 09:23:04 -0600 Subject: [PATCH] feat: add TestConnectionInfo for mobile Adds TestConnectionInfo component which exposes some internal app state to the jitsi-meet-torture through the UI accessibility layer. This component will render only if config.testing.testMode is set to true. --- config.js | 3 + .../components/Conference.native.js | 2 + react/features/testing/actionTypes.js | 9 + react/features/testing/actions.js | 20 ++ .../testing/components/AbstractTestHint.js | 23 ++ .../testing/components/TestConnectionInfo.js | 221 ++++++++++++++++++ .../testing/components/TestHint.android.js | 39 ++++ .../testing/components/TestHint.ios.js | 28 +++ react/features/testing/components/index.js | 2 + react/features/testing/index.js | 4 + react/features/testing/middleware.js | 80 +++++++ react/features/testing/reducer.js | 40 ++++ 12 files changed, 471 insertions(+) create mode 100644 react/features/testing/actionTypes.js create mode 100644 react/features/testing/actions.js create mode 100644 react/features/testing/components/AbstractTestHint.js create mode 100644 react/features/testing/components/TestConnectionInfo.js create mode 100644 react/features/testing/components/TestHint.android.js create mode 100644 react/features/testing/components/TestHint.ios.js create mode 100644 react/features/testing/components/index.js create mode 100644 react/features/testing/index.js create mode 100644 react/features/testing/middleware.js create mode 100644 react/features/testing/reducer.js diff --git a/config.js b/config.js index b609092d08..d183d8388e 100644 --- a/config.js +++ b/config.js @@ -57,6 +57,9 @@ var config = { // P2P test mode disables automatic switching to P2P when there are 2 // participants in the conference. p2pTestMode: false + + // Enables the test specific features consumed by jitsi-meet-torture + // testMode: false }, // Disables ICE/UDP by filtering out local and remote UDP candidates in diff --git a/react/features/conference/components/Conference.native.js b/react/features/conference/components/Conference.native.js index 9effb913cd..675854ba4a 100644 --- a/react/features/conference/components/Conference.native.js +++ b/react/features/conference/components/Conference.native.js @@ -15,6 +15,7 @@ import { createDesiredLocalTracks } from '../../base/tracks'; import { ConferenceNotification } from '../../calendar-sync'; import { Filmstrip } from '../../filmstrip'; import { LargeVideo } from '../../large-video'; +import { TestConnectionInfo } from '../../testing'; import { setToolboxVisible, Toolbox } from '../../toolbox'; import styles from './styles'; @@ -233,6 +234,7 @@ class Conference extends Component { */} + diff --git a/react/features/testing/actionTypes.js b/react/features/testing/actionTypes.js new file mode 100644 index 0000000000..331011266e --- /dev/null +++ b/react/features/testing/actionTypes.js @@ -0,0 +1,9 @@ +/** + * The type of redux action which sets the configuration of the feature + * base/logging. + * + * { + * type: SET_CONNECTION_STATE + * } + */ +export const SET_CONNECTION_STATE = Symbol('SET_CONNECTION_STATE'); diff --git a/react/features/testing/actions.js b/react/features/testing/actions.js new file mode 100644 index 0000000000..b65c732682 --- /dev/null +++ b/react/features/testing/actions.js @@ -0,0 +1,20 @@ +/* @flow */ + +import { SET_CONNECTION_STATE } from './actionTypes'; + +/** + * Sets the conference connection state of the testing feature. + * + * @param {string} connectionState - This is the lib-jitsi-meet event name. Can + * be on of: + * @returns {{ + * type: SET_CONNECTION_STATE, + * connectionState: string + * }} + */ +export function setConnectionState(connectionState: string) { + return { + type: SET_CONNECTION_STATE, + connectionState + }; +} diff --git a/react/features/testing/components/AbstractTestHint.js b/react/features/testing/components/AbstractTestHint.js new file mode 100644 index 0000000000..0544a1bcaf --- /dev/null +++ b/react/features/testing/components/AbstractTestHint.js @@ -0,0 +1,23 @@ +/* @flow */ + +/** + * Describes the {@link TestHint}'s properties. + * + * A test hint is meant to resemble the lack of the ability to execute + * JavaScript by the mobile torture tests. They are used to expose some of + * the app's internal state that is not always expressed in a feasible manner by + * the UI. + */ +export type TestHintProps = { + + /** + * The test hint's identifier string. Must be unique in the app instance + * scope. + */ + id: string, + + /** + * The test hint's (text) value which is to be consumed by the tests. + */ + value: string +} diff --git a/react/features/testing/components/TestConnectionInfo.js b/react/features/testing/components/TestConnectionInfo.js new file mode 100644 index 0000000000..908d4dccfb --- /dev/null +++ b/react/features/testing/components/TestConnectionInfo.js @@ -0,0 +1,221 @@ +// @flow + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + + +import { Fragment } from '../../base/react'; +import { getLocalParticipant } from '../../base/participants'; +import { statsEmitter } from '../../connection-indicator'; + +import { TestHint } from './index'; + +/** + * Defines the TestConnectionInfo's properties. + */ +type Props = { + + /** + * The JitsiConference's connection state. It's the lib-jitsi-meet's event + * name converted to a string directly. At the time of this writing these + * are the possible values: + * 'conference.connectionEstablished' + * 'conference.connectionInterrupted' + * 'conference.connectionRestored' + */ + _conferenceConnectionState: string, + + /** + * This will be a boolean converted to a string. The value will be 'true' + * once the conference is joined (the XMPP MUC room to be specific). + */ + _conferenceJoinedState: string, + + /** + * The local participant's ID. Required to be able to observe the local RTP + * stats. + */ + _localUserId: string, + + /** + * Indicates whether or not the test mode is currently on. Otherwise the + * TestConnectionInfo component will not render. + */ + _testMode: boolean +} + +/** + * Describes the TestConnectionInfo's state. + */ +type State = { + + /** + * The RTP stats section. + */ + stats: { + + /** + * The local bitrate. + */ + bitrate: { + + /** + * The local download RTP bitrate. + */ + download: number, + + /** + * The local upload RTP bitrate. + */ + upload: number + } + } +} + +/** + * The component will expose some of the app state to the jitsi-meet-torture + * through the UI accessibility layer which is visible to the tests. The Web + * tests currently will execute JavaScript and access globals variables to learn + * this information, but there's no such option on React Native(maybe that's + * a good thing). + */ +class TestConnectionInfo extends Component { + + _onStatsUpdated: Object => void + + /** + * Initializes new TestConnectionInfo instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props: Object) { + super(props); + + this._onStatsUpdated = this._onStatsUpdated.bind(this); + + this.state = { + stats: { + bitrate: { + download: 0, + upload: 0 + } + } + }; + } + + /** + * The {@link statsEmitter} callback hoked up for the local participant. + * + * @param {Object} stats - These are the RTP stats. Look in + * the lib-jitsi-meet for more details on the actual structure or add + * a console print and figure out there. + * @returns {void} + * @private + */ + _onStatsUpdated(stats = {}) { + this.setState({ + stats: { + bitrate: { + download: stats.bitrate.download, + upload: stats.bitrate.upload + } + } + }); + } + + /** + * Starts listening for the local RTP stat updates. + * + * @inheritdoc + * returns {void} + */ + componentDidMount() { + statsEmitter.subscribeToClientStats( + this.props._localUserId, this._onStatsUpdated); + } + + /** + * Updates which user's stats are being listened to (the local participant's + * id changes). + * + * @inheritdoc + * returns {void} + */ + componentDidUpdate(prevProps) { + if (prevProps._localUserId !== this.props._localUserId) { + statsEmitter.unsubscribeToClientStats( + prevProps._localUserId, this._onStatsUpdated); + statsEmitter.subscribeToClientStats( + this.props._localUserId, this._onStatsUpdated); + } + } + + /** + * Removes the local stats listener. + * + * @private + * @returns {void} + */ + componentWillUnmount() { + statsEmitter.unsubscribeToClientStats( + this.props._localUserId, this._onStatsUpdated); + } + + /** + * Renders the component if the app is currently running in the test mode + * (config.testing.testMode == true). + * + * @returns {ReactElement|null} + */ + render() { + if (!this.props._testMode) { + return null; + } + + return ( + + + + + + ); + } +} + + +/** + * Maps (parts of) the Redux state to the associated TestConnectionInfo's props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _conferenceConnectionState: string, + * _conferenceJoinedState: string, + * _localUserId: string, + * _testMode: boolean + * }} + */ +function _mapStateToProps(state) { + const conferenceJoined + = Boolean(state['features/base/conference'].conference); + const localParticipant = getLocalParticipant(state); + + const testingConfig = state['features/base/config'].testing; + const testMode = Boolean(testingConfig && testingConfig.testMode); + + return { + _conferenceConnectionState: state['features/testing'].connectionState, + _conferenceJoinedState: conferenceJoined.toString(), + _localUserId: localParticipant && localParticipant.id, + _testMode: testMode + }; +} + +export default connect(_mapStateToProps)(TestConnectionInfo); diff --git a/react/features/testing/components/TestHint.android.js b/react/features/testing/components/TestHint.android.js new file mode 100644 index 0000000000..e1f1c59e52 --- /dev/null +++ b/react/features/testing/components/TestHint.android.js @@ -0,0 +1,39 @@ +/* @flow */ + +import React, { Component } from 'react'; +import { Text } from 'react-native'; + +import type { TestHintProps } from './AbstractTestHint'; + +/** + * The Android version of TestHint. It will put the identifier, + * as the 'accessibilityLabel'. + * + * FIXME The 'testID' attribute (which is used on iOS) does not work with + * the react-native as expected, because is mapped to component's tag instead of + * any attribute visible to the UI automation. Because of that it can not be + * used to find the element. + * On the other hand it's not possible to use 'accessibilityLabel' on the iOS + * for the id purpose, because it will merge the value with any text content or + * 'accessibilityLabel' values of it's children. So as a workaround a TestHint + * class was introduced in 'jitsi-meet-torture' which will accept generic 'id' + * attribute and then do the search 'under the hood' either by the accessibility + * label or the id, depending on the participant's platform. On the client side + * the TestHint class is to be the abstraction layer which masks the problem by + * exposing id and value properties. + */ +export default class TestHint extends Component { + + /** + * Renders the test hint on Android. + * + * @returns {ReactElement} + */ + render() { + return ( + + { this.props.value } + + ); + } +} diff --git a/react/features/testing/components/TestHint.ios.js b/react/features/testing/components/TestHint.ios.js new file mode 100644 index 0000000000..b4158bec2e --- /dev/null +++ b/react/features/testing/components/TestHint.ios.js @@ -0,0 +1,28 @@ +/* @flow */ + +import React, { Component } from 'react'; +import { Text } from 'react-native'; + +import type { TestHintProps } from './AbstractTestHint'; + +/** + * This is the iOS version of the TestHint. + * + * Be sure to check the description in TestHint.android and AbstractTestHint + * files to understand what a test hint is and why different iOS and Android + * components are necessary. + */ +export default class TestHint extends Component { + /** + * Renders the test hint on Android. + * + * @returns {ReactElement} + */ + render() { + return ( + + ); + } +} diff --git a/react/features/testing/components/index.js b/react/features/testing/components/index.js new file mode 100644 index 0000000000..96d58b8238 --- /dev/null +++ b/react/features/testing/components/index.js @@ -0,0 +1,2 @@ +export { default as TestConnectionInfo } from './TestConnectionInfo'; +export { default as TestHint } from './TestHint'; diff --git a/react/features/testing/index.js b/react/features/testing/index.js new file mode 100644 index 0000000000..daaa5d02a7 --- /dev/null +++ b/react/features/testing/index.js @@ -0,0 +1,4 @@ +export * from './components'; + +import './middleware'; +import './reducer'; diff --git a/react/features/testing/middleware.js b/react/features/testing/middleware.js new file mode 100644 index 0000000000..1d34ceb86c --- /dev/null +++ b/react/features/testing/middleware.js @@ -0,0 +1,80 @@ +/* @flow */ + +import Logger from 'jitsi-meet-logger'; + +const logger = Logger.getLogger(__filename); + +import { MiddlewareRegistry } from '../base/redux'; + +import { CONFERENCE_WILL_JOIN } from '../base/conference'; +import { JitsiConferenceEvents } from '../base/lib-jitsi-meet'; +import { setConnectionState } from './actions'; + +/** + * The Redux middleware of the feature testing. + * + * @param {Store} store - The Redux store. + * @returns {Function} + * @private + */ +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case CONFERENCE_WILL_JOIN: + _bindConferenceConnectionListener(action.conference, store); + break; + } + + return next(action); +}); + +/** + * Binds a handler which will listen for the connection related conference + * events (in the lib-jitsi-meet internals those are associated with the ICE + * connection state). + * + * @param {JitsiConference} conference - The {@link JitsiConference} for which + * the conference will join even is dispatched. + * @param {Store} store - The redux store in which the specified action is being + * dispatched. + * @private + * @returns {void} + */ +function _bindConferenceConnectionListener(conference, { dispatch }) { + + conference.on( + JitsiConferenceEvents.CONNECTION_ESTABLISHED, + _onConnectionEvent.bind( + null, JitsiConferenceEvents.CONNECTION_ESTABLISHED, dispatch)); + conference.on( + JitsiConferenceEvents.CONNECTION_RESTORED, + _onConnectionEvent.bind( + null, JitsiConferenceEvents.CONNECTION_RESTORED, dispatch)); + conference.on( + JitsiConferenceEvents.CONNECTION_INTERRUPTED, + _onConnectionEvent.bind( + null, JitsiConferenceEvents.CONNECTION_INTERRUPTED, dispatch)); +} + +/** + * The handler function for conference connection events which wil store the + * latest even name in the Redux store of feature testing. + * + * @param {string} event - One of the lib-jitsi-meet JitsiConferenceEvents. + * @param {Function} dispatch - The dispatch function of the current Redux + * store. + * @returns {void} + * @private + */ +function _onConnectionEvent(event, dispatch) { + switch (event) { + case JitsiConferenceEvents.CONNECTION_ESTABLISHED: + case JitsiConferenceEvents.CONNECTION_INTERRUPTED: + case JitsiConferenceEvents.CONNECTION_RESTORED: + dispatch(setConnectionState(event)); + break; + default: + logger.error(`onConnectionEvent - unsupported event type: ${event}`); + break; + } +} + diff --git a/react/features/testing/reducer.js b/react/features/testing/reducer.js new file mode 100644 index 0000000000..fa81b7d72b --- /dev/null +++ b/react/features/testing/reducer.js @@ -0,0 +1,40 @@ +import { assign, ReducerRegistry } from '../base/redux'; + +import { SET_CONNECTION_STATE } from './actionTypes'; + +/** + * The initial state of the feature testing. + * + * @type {{ + * connectionState: string + * }} + */ +const INITIAL_STATE = { + connectionState: '' +}; + +ReducerRegistry.register( + 'features/testing', + (state = INITIAL_STATE, action) => { + switch (action.type) { + case SET_CONNECTION_STATE: + return _setConnectionState(state, action); + + default: + return state; + } + }); + +/** + * Reduces a specific Redux action SET_CONNECTION_STATE of the feature + * testing. + * + * @param {Object} state - The Redux state of the feature base/logging. + * @param {Action} action - The Redux action SET_CONNECTION_STATE to reduce. + * @private + * @returns {Object} The new state of the feature testing after the + * reduction of the specified action. + */ +function _setConnectionState(state, action) { + return assign(state, { connectionState: action.connectionState }); +}