Add tests for actions & reducers

This commit is contained in:
Jerko Steiner 2017-06-17 09:49:48 -04:00
parent 46ec853280
commit 300afd6b2f
21 changed files with 384 additions and 51 deletions

View File

@ -4,7 +4,8 @@
"rules": {
"max-len": [2, 80, 4],
"jsx-quotes": ["error", "prefer-double"],
"padded-blocks": 0
"padded-blocks": 0,
"import/first": 0
},
"globals": {
"expect": true,

View File

@ -52,7 +52,7 @@ lint-fix:
.PHONY: test
test:
jest --verbose
jest --forceExit
.PHONY: testify
testify:
@ -62,7 +62,7 @@ testify:
.PHONY: coverage
coverage:
jest --coverage
jest --coverage --forceExit
.PHONY: server
server:

View File

@ -1,8 +1,9 @@
import EventEmitter from 'events'
const Peer = jest.genMockFunction().mockImplementation(() => {
let peer = new EventEmitter()
peer.destroy = jest.genMockFunction()
peer.signal = jest.genMockFunction()
peer.destroy = jest.fn()
peer.signal = jest.fn()
peer.send = jest.fn()
Peer.instances.push(peer)
return peer
})

View File

@ -6,7 +6,7 @@
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"test": "jest --coverage",
"test": "jest --coverage --forceExit",
"testify": "jest --watch",
"lint": "eslint ./index.js ./src/js"
},

View File

@ -0,0 +1,2 @@
import EventEmitter from 'events'
export default new EventEmitter()

View File

@ -6,21 +6,23 @@ import getUserMedia from '../window/getUserMedia.js'
import handshake from '../peer/handshake.js'
import socket from '../socket.js'
export const init = () => dispatch => ({
type: constants.INIT,
payload: Promise.all([
connect()(dispatch),
getCameraStream()(dispatch)
])
.spread((socket, stream) => {
handshake.init({ socket, callId, stream })
export const init = () => dispatch => {
return dispatch({
type: constants.INIT,
payload: Promise.all([
connect()(dispatch),
getCameraStream()(dispatch)
])
.spread((socket, stream) => {
handshake({ socket, callId, stream })
})
})
})
}
export const connect = () => dispatch => {
return new Promise(resolve => {
socket.once('connect', () => {
dispatch(NotifyActions.warn('Connected to server socket'))
dispatch(NotifyActions.warning('Connected to server socket'))
resolve(socket)
})
socket.on('disconnect', () => {
@ -35,9 +37,9 @@ export const getCameraStream = () => dispatch => {
dispatch(addStream({ stream, userId: constants.ME }))
return stream
})
.catch(() => {
.catch(err => {
dispatch(NotifyActions.alert('Could not get access to microphone & camera'))
return null
throw err
})
}

View File

@ -1,4 +1,5 @@
import * as constants from '../constants.js'
import Immutable from 'seamless-immutable'
const TIMEOUT = 5000
@ -11,7 +12,7 @@ function format (string, args) {
const _notify = (type, args) => dispatch => {
let string = args[0] || ''
let message = format(string, Array.prototype.slice.call(args, 1))
const payload = { type, message }
const payload = Immutable({ type, message })
dispatch({
type: constants.NOTIFY,
payload
@ -24,18 +25,22 @@ const _notify = (type, args) => dispatch => {
}, TIMEOUT)
}
export const info = function() {
export const info = function () {
return dispatch => _notify('info', arguments)(dispatch)
}
export const warn = function() {
export const warning = function () {
return dispatch => _notify('warning', arguments)(dispatch)
}
export const error = function() {
export const error = function () {
return dispatch => _notify('error', arguments)(dispatch)
}
export const clear = () => ({
type: constants.NOTIFY_CLEAR
})
export function alert (message, dismissable) {
return {
type: constants.ALERT,
@ -48,9 +53,15 @@ export function alert (message, dismissable) {
}
}
export const dismiss = alert => {
export const dismissAlert = alert => {
return {
type: constants.ALERT_DISMISS,
payload: alert
}
}
export const clearAlerts = () => {
return {
type: constants.ALERT_CLEAR
}
}

View File

@ -0,0 +1,93 @@
jest.mock('../../callId.js')
jest.mock('../../iceServers.js')
jest.mock('../../peer/handshake.js')
jest.mock('../../socket.js')
jest.mock('../../window/getUserMedia.js')
jest.mock('../../store.js')
import * as CallActions from '../CallActions.js'
import * as constants from '../../constants.js'
import * as getUserMediaMock from '../../window/getUserMedia.js'
import callId from '../../callId.js'
import handshake from '../../peer/handshake.js'
import socket from '../../socket.js'
import store from '../../store.js'
jest.useFakeTimers()
describe('reducers/alerts', () => {
beforeEach(() => {
store.clearActions()
getUserMediaMock.fail(false)
})
afterEach(() => {
jest.runAllTimers()
socket.removeAllListeners('connect')
socket.removeAllListeners('disconnect')
})
describe('init', () => {
it('calls handshake.init when connected & got camera stream', done => {
const promise = store.dispatch(CallActions.init())
socket.emit('connect')
expect(store.getActions()).toEqual([{
type: constants.INIT_PENDING
}, {
type: constants.NOTIFY,
payload: {
message: 'Connected to server socket',
type: 'warning'
}
}])
promise.then(() => {
expect(handshake.mock.calls).toEqual([[{
socket,
callId,
stream: getUserMediaMock.stream
}]])
})
.then(done)
.catch(done.fail)
})
it('calls dispatches disconnect message on disconnect', done => {
const promise = store.dispatch(CallActions.init())
socket.emit('connect')
socket.emit('disconnect')
expect(store.getActions()).toEqual([{
type: constants.INIT_PENDING
}, {
type: constants.NOTIFY,
payload: {
message: 'Connected to server socket',
type: 'warning'
}
}, {
type: constants.NOTIFY,
payload: {
message: 'Server socket disconnected',
type: 'error'
}
}])
promise.then(done).catch(done.fail)
})
it('dispatches alert when failed to get media stream', done => {
getUserMediaMock.fail(true)
const promise = store.dispatch(CallActions.init())
socket.emit('connect')
promise
.then(done.fail)
.catch(err => {
expect(err.message).toEqual('test')
done()
})
})
})
})

View File

@ -8,6 +8,7 @@ import _ from 'underscore'
export default class App extends React.Component {
static propTypes = {
dismissAlert: PropTypes.func.isRequired,
streams: PropTypes.objectOf(StreamPropType).isRequired,
alerts: PropTypes.arrayOf(AlertPropType).isRequired,
activate: PropTypes.func.isRequired,
@ -21,11 +22,11 @@ export default class App extends React.Component {
}
render () {
const {
active, activate, alerts, dismiss, notify, notifications, streams
active, activate, alerts, dismissAlert, notify, notifications, streams
} = this.props
return (<div className="app">
<Alerts alerts={alerts} dismiss={dismiss} />
<Alerts alerts={alerts} dismiss={dismissAlert} />
<Notifications notifications={notifications} />
<Input notify={notify} />
<div className="videos">

View File

@ -2,9 +2,9 @@ import { PENDING, FULFILLED, REJECTED } from 'redux-promise-middleware'
export const ME = '_me_'
export const INIT = 'INIT'
export const INIT_PENDING = `${INIT}${PENDING}`
export const INIT_FULFILLED = `${INIT}${FULFILLED}`
export const INIT_REJECTED = `${INIT}${REJECTED}`
export const INIT_PENDING = `${INIT}_${PENDING}`
export const INIT_FULFILLED = `${INIT}_${FULFILLED}`
export const INIT_REJECTED = `${INIT}_${REJECTED}`
export const ALERT = 'ALERT'
export const ALERT_DISMISS = 'ALERT_DISMISS'

View File

@ -18,7 +18,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
activate: bindActionCreators(CallActions.activateStream, dispatch),
dismiss: bindActionCreators(NotifyActions.dismiss, dispatch),
dismissAlert: bindActionCreators(NotifyActions.dismissAlert, dispatch),
init: bindActionCreators(CallActions.init, dispatch),
notify: bindActionCreators(NotifyActions.info, dispatch)
}

View File

@ -4,7 +4,7 @@ jest.mock('../../callId.js')
jest.mock('../../iceServers.js')
import * as constants from '../../constants.js'
import * as handshake from '../handshake.js'
import handshake from '../handshake.js'
import Peer from 'simple-peer'
import peers from '../peers.js'
import store from '../../store.js'
@ -25,7 +25,7 @@ describe('handshake', () => {
describe('socket events', () => {
describe('users', () => {
it('add a peer for each new user and destroy peers for missing', () => {
handshake.init(socket, 'bla')
handshake(socket, 'bla')
// given
let payload = {
@ -53,7 +53,7 @@ describe('handshake', () => {
let data
beforeEach(() => {
data = {}
handshake.init(socket, 'bla')
handshake(socket, 'bla')
socket.emit('users', {
initiator: 'a',
users: [{ id: 'a' }, { id: 'b' }]
@ -88,7 +88,7 @@ describe('handshake', () => {
let ready = false
socket.once('ready', () => { ready = true })
handshake.init(socket, 'bla')
handshake(socket, 'bla')
socket.emit('users', {
initiator: 'a',

View File

@ -26,8 +26,6 @@ import store from '../../store.js'
import { EventEmitter } from 'events'
import { play } from '../../window/video.js'
const { dispatch } = store
describe('peers', () => {
function createSocket () {
const socket = new EventEmitter()
@ -71,7 +69,6 @@ describe('peers', () => {
peers.create({ socket, user, initiator: 'user2', stream })
expect(store.getActions()).toEqual([actions.connecting])
// expect(notify.warn.mock.calls).toEqual([[ 'Connecting to peer...' ]])
expect(Peer.instances.length).toBe(1)
expect(Peer.mock.calls.length).toBe(1)
@ -121,6 +118,34 @@ describe('peers', () => {
expect(play.mock.calls.length).toBe(1)
})
})
describe('data', () => {
beforeEach(() => {
window.TextDecoder = class TextDecoder {
constructor(encoding) {
this.encoding = encoding
}
decode(object) {
return object.toString(this.encoding)
}
}
})
it('decodes a message', () => {
store.clearActions()
const message = 'test'
const object = JSON.stringify({ message })
peer.emit('data', Buffer.from(object, 'utf-8'))
expect(store.getActions()).toEqual([{
type: constants.NOTIFY,
payload: {
type: 'info',
message: `${user.id}: ${message}`
}
}])
})
})
})
describe('get', () => {
@ -138,10 +163,10 @@ describe('peers', () => {
describe('getIds', () => {
it('returns ids of all peers', () => {
peers.create({
socket, user: {id: 'user2' }, initiator: 'user2', stream
socket, user: { id: 'user2' }, initiator: 'user2', stream
})
peers.create({
socket, user: {id: 'user3' }, initiator: 'user3', stream
socket, user: { id: 'user3' }, initiator: 'user3', stream
})
expect(peers.getIds()).toEqual([ 'user2', 'user3' ])
@ -165,10 +190,10 @@ describe('peers', () => {
describe('clear', () => {
it('destroys all peers and removes them', () => {
peers.create({
socket, user: {id: 'user2' }, initiator: 'user2', stream
socket, user: { id: 'user2' }, initiator: 'user2', stream
})
peers.create({
socket, user: {id: 'user3' }, initiator: 'user3', stream
socket, user: { id: 'user3' }, initiator: 'user3', stream
})
peers.clear()
@ -179,4 +204,22 @@ describe('peers', () => {
expect(peers.getIds()).toEqual([])
})
})
describe('message', () => {
it('sends a message to all peers', () => {
peers.create({
socket, user: { id: 'user2' }, initiator: 'user2', stream
})
peers.create({
socket, user: { id: 'user3' }, initiator: 'user3', stream
})
peers.message('test')
expect(peers.get('user2').send.mock.calls)
.toEqual([[ '{"message":"test"}' ]])
expect(peers.get('user3').send.mock.calls)
.toEqual([[ '{"message":"test"}' ]])
})
})
})

View File

@ -7,7 +7,7 @@ import store from '../store.js'
const debug = _debug('peercalls')
const { dispatch } = store
export function init (socket, roomName, stream) {
export default function init (socket, roomName, stream) {
function createPeer (user, initiator) {
return peers.create({ socket, user, initiator, stream })
}

View File

@ -22,7 +22,7 @@ let peers = {}
function create ({ socket, user, initiator, stream }) {
debug('create peer: %s, stream:', user.id, stream)
dispatch(
NotifyActions.warn('Connecting to peer...')
NotifyActions.warning('Connecting to peer...')
)
if (peers[user.id]) {
@ -56,7 +56,7 @@ function create ({ socket, user, initiator, stream }) {
peer.once('connect', () => {
debug('peer: %s, connect', user.id)
dispatch(
NotifyActions.warn('Peer connection established')
NotifyActions.warning('Peer connection established')
)
play()
})
@ -86,9 +86,7 @@ function create ({ socket, user, initiator, stream }) {
CallActions.removeStream(user.id)
)
// make sure some other peer with same id didn't take place between calling
// `destroy()` and `close` event
if (peers[user.id] === peer) delete peers[user.id]
delete peers[user.id]
})
}

View File

@ -0,0 +1,95 @@
import * as NotifyActions from '../../actions/NotifyActions.js'
import { applyMiddleware, createStore } from 'redux'
import { create } from '../../middlewares.js'
import reducers from '../index.js'
jest.useFakeTimers()
describe('reducers/alerts', () => {
let store
beforeEach(() => {
store = createStore(
reducers,
applyMiddleware.apply(null, create())
)
})
describe('clearAlert', () => {
const actions = {
true: 'Dismiss',
false: ''
}
;[true, false].forEach(dismissable => {
beforeEach(() => {
store.dispatch(NotifyActions.clearAlerts())
})
it('adds alert to store', () => {
store.dispatch(NotifyActions.alert('test', dismissable))
expect(store.getState().alerts).toEqual([{
action: actions[dismissable],
dismissable,
message: 'test',
type: 'warning'
}])
})
})
})
describe('dismissAlert', () => {
it('removes an alert', () => {
store.dispatch(NotifyActions.alert('test', true))
expect(store.getState().alerts.length).toBe(1)
store.dispatch(NotifyActions.dismissAlert(store.getState().alerts[0]))
expect(store.getState().alerts.length).toBe(0)
})
it('does not remove an alert when not found', () => {
store.dispatch(NotifyActions.alert('test', true))
expect(store.getState().alerts.length).toBe(1)
store.dispatch(NotifyActions.dismissAlert({}))
expect(store.getState().alerts.length).toBe(1)
})
})
;['info', 'warning', 'error'].forEach(type => {
describe(type, () => {
beforeEach(() => {
store.dispatch(NotifyActions[type]('Hi {0}!', 'John'))
})
it('adds a notification', () => {
expect(store.getState().notifications).toEqual([{
message: 'Hi John!',
type
}])
})
it('dismisses notification after a timeout', () => {
jest.runAllTimers()
expect(store.getState().notifications).toEqual([])
})
})
})
describe('clear', () => {
it('clears all alerts', () => {
store.dispatch(NotifyActions.info('Hi {0}!', 'John'))
store.dispatch(NotifyActions.warning('Hi {0}!', 'John'))
store.dispatch(NotifyActions.error('Hi {0}!', 'John'))
store.dispatch(NotifyActions.clear())
expect(store.getState().notifications).toEqual([])
})
})
})

View File

@ -0,0 +1,70 @@
jest.mock('../../callId.js')
jest.mock('../../iceServers.js')
jest.mock('../../window/createObjectURL.js')
import * as CallActions from '../../actions/CallActions.js'
import { applyMiddleware, createStore } from 'redux'
import { create } from '../../middlewares.js'
import reducers from '../index.js'
describe('reducers/alerts', () => {
class MediaStream {}
let store, stream, userId
beforeEach(() => {
store = createStore(
reducers,
applyMiddleware.apply(null, create())
)
userId = 'test id'
stream = new MediaStream()
})
describe('defaultState', () => {
it('should have default state set', () => {
expect(store.getState().streams).toEqual({
active: null,
all: {}
})
})
})
describe('addStream', () => {
it('adds a stream', () => {
store.dispatch(CallActions.addStream({ userId, stream }))
expect(store.getState().streams).toEqual({
active: userId,
all: {
[userId]: {
userId,
stream,
url: jasmine.any(String)
}
}
})
})
})
describe('removeStream', () => {
it('removes a stream', () => {
store.dispatch(CallActions.addStream({ userId, stream }))
store.dispatch(CallActions.removeStream(userId))
expect(store.getState().streams).toEqual({
active: userId,
all: {}
})
})
})
describe('activateStream', () => {
it('activates a stream', () => {
store.dispatch(CallActions.activateStream(userId))
expect(store.getState().streams).toEqual({
active: userId,
all: {}
})
})
})
})

View File

@ -6,7 +6,9 @@ const defaultState = Immutable([])
export default function alerts (state = defaultState, action) {
switch (action && action.type) {
case constants.ALERT:
return Immutable(state.asMutable().push(action.payload))
const alerts = state.asMutable()
alerts.push(action.payload)
return Immutable(alerts)
case constants.ALERT_DISMISS:
return state.filter(a => a !== action.payload)
case constants.ALERT_CLEAR:

View File

@ -9,19 +9,19 @@ const defaultState = Immutable({
function addStream (state, action) {
const { userId, stream } = action.payload
const streams = state.all.merge({
const all = state.all.merge({
[userId]: {
userId,
stream,
url: createObjectURL(stream)
}
})
return { active: userId, streams }
return state.merge({ active: userId, all })
}
function removeStream (state, action) {
const streams = state.all.without(action.payload.userId)
return state.merge({ streams })
const all = state.all.without(action.payload.userId)
return state.merge({ all })
}
export default function streams (state = defaultState, action) {

View File

@ -0,0 +1 @@
export default object => 'blob://' + String(object)

View File

@ -0,0 +1,13 @@
import Promise from 'bluebird'
class MediaStream {}
let shouldFail
export const fail = _fail => shouldFail = !!_fail
export const stream = new MediaStream()
export default function getUserMedia () {
return !shouldFail
? Promise.resolve(stream)
: Promise.reject(new Error('test'))
}