Increase test coverage

This commit is contained in:
Jerko Steiner 2017-06-17 10:43:01 -04:00
parent 300afd6b2f
commit 06821e1fce
19 changed files with 137 additions and 52 deletions

View File

@ -2,6 +2,7 @@ jest.mock('../actions/CallActions.js')
jest.mock('../callId.js') jest.mock('../callId.js')
jest.mock('../iceServers.js') jest.mock('../iceServers.js')
import * as constants from '../constants.js'
import App from '../containers/App.js' import App from '../containers/App.js'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
@ -14,9 +15,11 @@ import { middlewares } from '../store.js'
describe('App', () => { describe('App', () => {
const initAction = { type: 'INIT' }
let state let state
beforeEach(() => { beforeEach(() => {
init.mockReturnValue({ type: 'INIT' }) init.mockReturnValue(initAction)
state = reducers() state = reducers()
}) })
@ -39,4 +42,56 @@ describe('App', () => {
}) })
}) })
describe('state', () => {
let alert
beforeEach(() => {
state.streams = state.streams.setIn(['all'], {
'test': {
userId: 'test',
url: 'blob://'
}
})
state.notifications = state.notifications.merge({
'notification1': {
id: 'notification1',
message: 'test',
type: 'warning'
}
})
const alerts = state.alerts.asMutable()
alert = {
dismissable: true,
action: 'Dismiss',
message: 'test alert'
}
alerts.push(alert)
state.alerts = alerts
render()
store.clearActions()
})
describe('alerts', () => {
it('can be dismissed', () => {
const dismiss = node.querySelector('.action-alert-dismiss')
TestUtils.Simulate.click(dismiss)
expect(store.getActions()).toEqual([{
type: constants.ALERT_DISMISS,
payload: alert
}])
})
})
describe('video', () => {
it('can be activated', () => {
const video = node.querySelector('video')
TestUtils.Simulate.click(video)
expect(store.getActions()).toEqual([{
type: constants.STREAM_ACTIVATE,
payload: { userId: 'test' }
}])
})
})
})
}) })

View File

@ -0,0 +1,11 @@
window.localStorage = { debug: true }
import logger from 'redux-logger'
const store = require('../store.js')
describe('store', () => {
it('should load logger middleware', () => {
expect(store.middlewares.some(m => m === logger)).toBeTruthy()
})
})

View File

@ -1,3 +1,4 @@
import * as StreamActions from './StreamActions.js'
import * as NotifyActions from './NotifyActions.js' import * as NotifyActions from './NotifyActions.js'
import * as constants from '../constants.js' import * as constants from '../constants.js'
import Promise from 'bluebird' import Promise from 'bluebird'
@ -34,7 +35,7 @@ export const connect = () => dispatch => {
export const getCameraStream = () => dispatch => { export const getCameraStream = () => dispatch => {
return getUserMedia({ video: true, audio: true }) return getUserMedia({ video: true, audio: true })
.then(stream => { .then(stream => {
dispatch(addStream({ stream, userId: constants.ME })) dispatch(StreamActions.addStream({ stream, userId: constants.ME }))
return stream return stream
}) })
.catch(err => { .catch(err => {
@ -42,21 +43,3 @@ export const getCameraStream = () => dispatch => {
throw err throw err
}) })
} }
export const addStream = ({ stream, userId }) => ({
type: constants.STREAM_ADD,
payload: {
userId,
stream
}
})
export const removeStream = userId => ({
type: constants.STREAM_REMOVE,
payload: { userId }
})
export const activateStream = userId => ({
type: constants.STREAM_ACTIVATE,
payload: { userId }
})

View File

@ -1,5 +1,5 @@
import * as constants from '../constants.js' import * as constants from '../constants.js'
import Immutable from 'seamless-immutable' import _ from 'underscore'
const TIMEOUT = 5000 const TIMEOUT = 5000
@ -12,7 +12,8 @@ function format (string, args) {
const _notify = (type, args) => dispatch => { const _notify = (type, args) => dispatch => {
let string = args[0] || '' let string = args[0] || ''
let message = format(string, Array.prototype.slice.call(args, 1)) let message = format(string, Array.prototype.slice.call(args, 1))
const payload = Immutable({ type, message }) const id = _.uniqueId('notification')
const payload = { id, type, message }
dispatch({ dispatch({
type: constants.NOTIFY, type: constants.NOTIFY,
payload payload
@ -20,7 +21,7 @@ const _notify = (type, args) => dispatch => {
setTimeout(() => { setTimeout(() => {
dispatch({ dispatch({
type: constants.NOTIFY_DISMISS, type: constants.NOTIFY_DISMISS,
payload payload: { id }
}) })
}, TIMEOUT) }, TIMEOUT)
} }

View File

@ -0,0 +1,19 @@
import * as constants from '../constants.js'
export const addStream = ({ stream, userId }) => ({
type: constants.STREAM_ADD,
payload: {
userId,
stream
}
})
export const removeStream = userId => ({
type: constants.STREAM_REMOVE,
payload: { userId }
})
export const activateStream = userId => ({
type: constants.STREAM_ACTIVATE,
payload: { userId }
})

View File

@ -38,6 +38,7 @@ describe('reducers/alerts', () => {
}, { }, {
type: constants.NOTIFY, type: constants.NOTIFY,
payload: { payload: {
id: jasmine.any(String),
message: 'Connected to server socket', message: 'Connected to server socket',
type: 'warning' type: 'warning'
} }
@ -62,12 +63,14 @@ describe('reducers/alerts', () => {
}, { }, {
type: constants.NOTIFY, type: constants.NOTIFY,
payload: { payload: {
id: jasmine.any(String),
message: 'Connected to server socket', message: 'Connected to server socket',
type: 'warning' type: 'warning'
} }
}, { }, {
type: constants.NOTIFY, type: constants.NOTIFY,
payload: { payload: {
id: jasmine.any(String),
message: 'Server socket disconnected', message: 'Server socket disconnected',
type: 'error' type: 'error'
} }

View File

@ -18,13 +18,16 @@ export class Alert extends React.Component {
dismiss(alert) dismiss(alert)
} }
render () { render () {
const { alert, dismiss } = this.props const { alert } = this.props
return ( return (
<div className={classnames('alert', alert.type)}> <div className={classnames('alert', alert.type)}>
<span>{alert.message}</span> <span>{alert.message}</span>
{alert.dismissable && ( {alert.dismissable && (
<button onClick={dismiss}>{alert.action}</button> <button
className="action-alert-dismiss"
onClick={this.dismiss}
>{alert.action}</button>
)} )}
</div> </div>
) )

View File

@ -34,6 +34,7 @@ export default class App extends React.Component {
<Video <Video
activate={activate} activate={activate}
active={userId === active} active={userId === active}
key={userId}
stream={stream} stream={stream}
/> />
))} ))}

View File

@ -11,7 +11,7 @@ export const NotificationPropTypes = PropTypes.shape({
export default class Notifications extends React.Component { export default class Notifications extends React.Component {
static propTypes = { static propTypes = {
notifications: PropTypes.arrayOf(NotificationPropTypes).isRequired, notifications: PropTypes.objectOf(NotificationPropTypes).isRequired,
max: PropTypes.number.isRequired max: PropTypes.number.isRequired
} }
static defaultProps = { static defaultProps = {
@ -26,12 +26,12 @@ export default class Notifications extends React.Component {
transitionLeaveTimeout={100} transitionLeaveTimeout={100}
transitionName="fade" transitionName="fade"
> >
{notifications.slice(max).map(notification => ( {Object.keys(notifications).slice(-max).map(id => (
<div <div
className={classnames(notification.type, 'notification')} className={classnames(notifications[id].type, 'notification')}
key={notification.id} key={id}
> >
{notification.message} {notifications[id].message}
</div> </div>
))} ))}
</CSSTransitionGroup> </CSSTransitionGroup>

View File

@ -5,14 +5,13 @@ import { ME } from '../constants.js'
export const StreamPropType = PropTypes.shape({ export const StreamPropType = PropTypes.shape({
userId: PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
stream: PropTypes.instanceOf(ArrayBuffer).isRequired,
url: PropTypes.string.isRequired url: PropTypes.string.isRequired
}) })
export default class Video extends React.Component { export default class Video extends React.Component {
static propTypes = { static propTypes = {
activate: PropTypes.func.isRequired, activate: PropTypes.func.isRequired,
active: PropTypes.string.required, active: PropTypes.bool.isRequired,
stream: StreamPropType.isRequired stream: StreamPropType.isRequired
} }
activate = e => { activate = e => {

View File

@ -1,5 +1,6 @@
import * as NotifyActions from '../actions/NotifyActions.js'
import * as CallActions from '../actions/CallActions.js' import * as CallActions from '../actions/CallActions.js'
import * as NotifyActions from '../actions/NotifyActions.js'
import * as StreamActions from '../actions/StreamActions.js'
import App from '../components/App.js' import App from '../components/App.js'
import React from 'react' import React from 'react'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
@ -17,7 +18,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
activate: bindActionCreators(CallActions.activateStream, dispatch), activate: bindActionCreators(StreamActions.activateStream, dispatch),
dismissAlert: bindActionCreators(NotifyActions.dismissAlert, dispatch), dismissAlert: bindActionCreators(NotifyActions.dismissAlert, dispatch),
init: bindActionCreators(CallActions.init, dispatch), init: bindActionCreators(CallActions.init, dispatch),
notify: bindActionCreators(NotifyActions.info, dispatch) notify: bindActionCreators(NotifyActions.info, dispatch)

View File

@ -145,6 +145,7 @@ describe('handshake', () => {
expect(store.getActions()).toEqual([{ expect(store.getActions()).toEqual([{
type: constants.NOTIFY, type: constants.NOTIFY,
payload: { payload: {
id: jasmine.any(String),
message: 'Peer connection closed', message: 'Peer connection closed',
type: 'error' type: 'error'
} }

View File

@ -49,6 +49,7 @@ describe('peers', () => {
connecting: { connecting: {
type: constants.NOTIFY, type: constants.NOTIFY,
payload: { payload: {
id: jasmine.any(String),
message: 'Connecting to peer...', message: 'Connecting to peer...',
type: 'warning' type: 'warning'
} }
@ -56,6 +57,7 @@ describe('peers', () => {
established: { established: {
type: constants.NOTIFY, type: constants.NOTIFY,
payload: { payload: {
id: jasmine.any(String),
message: 'Peer connection established', message: 'Peer connection established',
type: 'warning' type: 'warning'
} }
@ -140,6 +142,7 @@ describe('peers', () => {
expect(store.getActions()).toEqual([{ expect(store.getActions()).toEqual([{
type: constants.NOTIFY, type: constants.NOTIFY,
payload: { payload: {
id: jasmine.any(String),
type: 'info', type: 'info',
message: `${user.id}: ${message}` message: `${user.id}: ${message}`
} }

View File

@ -1,5 +1,6 @@
import * as CallActions from '../actions/CallActions.js' import * as CallActions from '../actions/CallActions.js'
import * as NotifyActions from '../actions/NotifyActions.js' import * as NotifyActions from '../actions/NotifyActions.js'
import * as StreamActions from '../actions/StreamActions.js'
import Peer from 'simple-peer' import Peer from 'simple-peer'
import _ from 'underscore' import _ from 'underscore'
import _debug from 'debug' import _debug from 'debug'
@ -63,7 +64,7 @@ function create ({ socket, user, initiator, stream }) {
peer.on('stream', stream => { peer.on('stream', stream => {
debug('peer: %s, stream', user.id) debug('peer: %s, stream', user.id)
dispatch(CallActions.addStream({ dispatch(StreamActions.addStream({
userId: user.id, userId: user.id,
stream stream
})) }))
@ -83,7 +84,7 @@ function create ({ socket, user, initiator, stream }) {
NotifyActions.error('Peer connection closed') NotifyActions.error('Peer connection closed')
) )
dispatch( dispatch(
CallActions.removeStream(user.id) StreamActions.removeStream(user.id)
) )
delete peers[user.id] delete peers[user.id]

View File

@ -1,4 +1,5 @@
import * as NotifyActions from '../../actions/NotifyActions.js' import * as NotifyActions from '../../actions/NotifyActions.js'
import _ from 'underscore'
import { applyMiddleware, createStore } from 'redux' import { applyMiddleware, createStore } from 'redux'
import { create } from '../../middlewares.js' import { create } from '../../middlewares.js'
import reducers from '../index.js' import reducers from '../index.js'
@ -65,7 +66,8 @@ describe('reducers/alerts', () => {
}) })
it('adds a notification', () => { it('adds a notification', () => {
expect(store.getState().notifications).toEqual([{ expect(_.values(store.getState().notifications)).toEqual([{
id: jasmine.any(String),
message: 'Hi John!', message: 'Hi John!',
type type
}]) }])
@ -73,9 +75,14 @@ describe('reducers/alerts', () => {
it('dismisses notification after a timeout', () => { it('dismisses notification after a timeout', () => {
jest.runAllTimers() jest.runAllTimers()
expect(store.getState().notifications).toEqual([]) expect(store.getState().notifications).toEqual({})
}) })
it('does not fail when no arguments', () => {
store.dispatch(NotifyActions[type]())
})
}) })
}) })
@ -87,7 +94,7 @@ describe('reducers/alerts', () => {
store.dispatch(NotifyActions.warning('Hi {0}!', 'John')) store.dispatch(NotifyActions.warning('Hi {0}!', 'John'))
store.dispatch(NotifyActions.error('Hi {0}!', 'John')) store.dispatch(NotifyActions.error('Hi {0}!', 'John'))
store.dispatch(NotifyActions.clear()) store.dispatch(NotifyActions.clear())
expect(store.getState().notifications).toEqual([]) expect(store.getState().notifications).toEqual({})
}) })
}) })

View File

@ -2,7 +2,7 @@ jest.mock('../../callId.js')
jest.mock('../../iceServers.js') jest.mock('../../iceServers.js')
jest.mock('../../window/createObjectURL.js') jest.mock('../../window/createObjectURL.js')
import * as CallActions from '../../actions/CallActions.js' import * as StreamActions from '../../actions/StreamActions.js'
import { applyMiddleware, createStore } from 'redux' import { applyMiddleware, createStore } from 'redux'
import { create } from '../../middlewares.js' import { create } from '../../middlewares.js'
import reducers from '../index.js' import reducers from '../index.js'
@ -31,13 +31,12 @@ describe('reducers/alerts', () => {
describe('addStream', () => { describe('addStream', () => {
it('adds a stream', () => { it('adds a stream', () => {
store.dispatch(CallActions.addStream({ userId, stream })) store.dispatch(StreamActions.addStream({ userId, stream }))
expect(store.getState().streams).toEqual({ expect(store.getState().streams).toEqual({
active: userId, active: userId,
all: { all: {
[userId]: { [userId]: {
userId, userId,
stream,
url: jasmine.any(String) url: jasmine.any(String)
} }
} }
@ -47,8 +46,8 @@ describe('reducers/alerts', () => {
describe('removeStream', () => { describe('removeStream', () => {
it('removes a stream', () => { it('removes a stream', () => {
store.dispatch(CallActions.addStream({ userId, stream })) store.dispatch(StreamActions.addStream({ userId, stream }))
store.dispatch(CallActions.removeStream(userId)) store.dispatch(StreamActions.removeStream(userId))
expect(store.getState().streams).toEqual({ expect(store.getState().streams).toEqual({
active: userId, active: userId,
all: {} all: {}
@ -58,7 +57,7 @@ describe('reducers/alerts', () => {
describe('activateStream', () => { describe('activateStream', () => {
it('activates a stream', () => { it('activates a stream', () => {
store.dispatch(CallActions.activateStream(userId)) store.dispatch(StreamActions.activateStream(userId))
expect(store.getState().streams).toEqual({ expect(store.getState().streams).toEqual({
active: userId, active: userId,
all: {} all: {}

View File

@ -1,16 +1,16 @@
import * as constants from '../constants.js' import * as constants from '../constants.js'
import Immutable from 'seamless-immutable' import Immutable from 'seamless-immutable'
const defaultState = Immutable([]) const defaultState = Immutable({})
export default function notifications (state = defaultState, action) { export default function notifications (state = defaultState, action) {
switch (action && action.type) { switch (action && action.type) {
case constants.NOTIFY: case constants.NOTIFY:
const notifications = state.asMutable() return state.merge({
notifications.push(action.payload) [action.payload.id]: action.payload
return Immutable(notifications) })
case constants.NOTIFY_DISMISS: case constants.NOTIFY_DISMISS:
return state.filter(n => n !== action.payload) return state.without(action.payload.id)
case constants.NOTIFY_CLEAR: case constants.NOTIFY_CLEAR:
return defaultState return defaultState
default: default:

View File

@ -12,7 +12,6 @@ function addStream (state, action) {
const all = state.all.merge({ const all = state.all.merge({
[userId]: { [userId]: {
userId, userId,
stream,
url: createObjectURL(stream) url: createObjectURL(stream)
} }
}) })

View File

@ -1,7 +1,6 @@
import { create } from './middlewares.js' import { create } from './middlewares.js'
import reducers from './reducers' import reducers from './reducers'
import { applyMiddleware, createStore } from 'redux' import { applyMiddleware, createStore } from 'redux'
export const middlewares = create( export const middlewares = create(
window.localStorage && window.localStorage.debug window.localStorage && window.localStorage.debug
) )