mirror of https://github.com/jitsi/jitsi-meet
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.pull/2794/head
parent
461540d874
commit
4d942440db
@ -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'); |
@ -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 |
||||
}; |
||||
} |
@ -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 |
||||
} |
@ -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<Props, State> { |
||||
|
||||
_onStatsUpdated: Object => void |
||||
|
||||
/** |
||||
* Initializes new <tt>TestConnectionInfo</tt> 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 ( |
||||
<Fragment accessible = { false } > |
||||
<TestHint |
||||
id = 'org.jitsi.meet.conference.connectionState' |
||||
value = { this.props._conferenceConnectionState } /> |
||||
<TestHint |
||||
id = 'org.jitsi.meet.conference.joinedState' |
||||
value = { this.props._conferenceJoinedState } /> |
||||
<TestHint |
||||
id = 'org.jitsi.meet.stats.rtp' |
||||
value = { JSON.stringify(this.state.stats) } /> |
||||
</Fragment> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 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); |
@ -0,0 +1,39 @@ |
||||
/* @flow */ |
||||
|
||||
import React, { Component } from 'react'; |
||||
import { Text } from 'react-native'; |
||||
|
||||
import type { TestHintProps } from './AbstractTestHint'; |
||||
|
||||
/** |
||||
* The Android version of <code>TestHint</code>. 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<TestHintProps> { |
||||
|
||||
/** |
||||
* Renders the test hint on Android. |
||||
* |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<Text accessibilityLabel = { this.props.id } > |
||||
{ this.props.value } |
||||
</Text> |
||||
); |
||||
} |
||||
} |
@ -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<TestHintProps> { |
||||
/** |
||||
* Renders the test hint on Android. |
||||
* |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<Text |
||||
accessibilityLabel = { this.props.value } |
||||
testID = { this.props.id } /> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
export { default as TestConnectionInfo } from './TestConnectionInfo'; |
||||
export { default as TestHint } from './TestHint'; |
@ -0,0 +1,4 @@ |
||||
export * from './components'; |
||||
|
||||
import './middleware'; |
||||
import './reducer'; |
@ -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; |
||||
} |
||||
} |
||||
|
@ -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 }); |
||||
} |
Loading…
Reference in new issue