From 300afd6b2fcf610ba0520ca99388902eb979626b Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Sat, 17 Jun 2017 09:49:48 -0400 Subject: [PATCH] Add tests for actions & reducers --- .eslintrc | 3 +- Makefile | 4 +- __mocks__/simple-peer.js | 5 +- package.json | 2 +- src/client/__mocks__/socket.js | 2 + src/client/actions/CallActions.js | 26 ++--- src/client/actions/NotifyActions.js | 21 +++- .../actions/__tests__/CallActions-test.js | 93 ++++++++++++++++++ src/client/components/App.js | 5 +- src/client/constants.js | 6 +- src/client/containers/App.js | 2 +- src/client/peer/__tests__/handshake-test.js | 8 +- src/client/peer/__tests__/peers-test.js | 57 +++++++++-- src/client/peer/handshake.js | 2 +- src/client/peer/peers.js | 8 +- src/client/reducers/__tests__/alerts-test.js | 95 +++++++++++++++++++ src/client/reducers/__tests__/streams-test.js | 70 ++++++++++++++ src/client/reducers/alerts.js | 4 +- src/client/reducers/streams.js | 8 +- .../window/__mocks__/createObjectURL.js | 1 + src/client/window/__mocks__/getUserMedia.js | 13 +++ 21 files changed, 384 insertions(+), 51 deletions(-) create mode 100644 src/client/__mocks__/socket.js create mode 100644 src/client/actions/__tests__/CallActions-test.js create mode 100644 src/client/reducers/__tests__/alerts-test.js create mode 100644 src/client/reducers/__tests__/streams-test.js create mode 100644 src/client/window/__mocks__/createObjectURL.js create mode 100644 src/client/window/__mocks__/getUserMedia.js diff --git a/.eslintrc b/.eslintrc index 1928063..29fb07b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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, diff --git a/Makefile b/Makefile index 128ce0c..623ebd4 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/__mocks__/simple-peer.js b/__mocks__/simple-peer.js index 2d4e2b9..482f4e6 100644 --- a/__mocks__/simple-peer.js +++ b/__mocks__/simple-peer.js @@ -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 }) diff --git a/package.json b/package.json index 1acdbe9..39814fc 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/client/__mocks__/socket.js b/src/client/__mocks__/socket.js new file mode 100644 index 0000000..5fe4999 --- /dev/null +++ b/src/client/__mocks__/socket.js @@ -0,0 +1,2 @@ +import EventEmitter from 'events' +export default new EventEmitter() diff --git a/src/client/actions/CallActions.js b/src/client/actions/CallActions.js index 28e2e05..4ef22f0 100644 --- a/src/client/actions/CallActions.js +++ b/src/client/actions/CallActions.js @@ -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 }) } diff --git a/src/client/actions/NotifyActions.js b/src/client/actions/NotifyActions.js index 531cd5c..72ff674 100644 --- a/src/client/actions/NotifyActions.js +++ b/src/client/actions/NotifyActions.js @@ -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 + } +} diff --git a/src/client/actions/__tests__/CallActions-test.js b/src/client/actions/__tests__/CallActions-test.js new file mode 100644 index 0000000..3e7b865 --- /dev/null +++ b/src/client/actions/__tests__/CallActions-test.js @@ -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() + }) + }) + + }) + +}) + diff --git a/src/client/components/App.js b/src/client/components/App.js index 8ee7eb3..86f14f0 100644 --- a/src/client/components/App.js +++ b/src/client/components/App.js @@ -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 (
- +
diff --git a/src/client/constants.js b/src/client/constants.js index 82745b6..acfb748 100644 --- a/src/client/constants.js +++ b/src/client/constants.js @@ -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' diff --git a/src/client/containers/App.js b/src/client/containers/App.js index 5e3514e..73daeea 100644 --- a/src/client/containers/App.js +++ b/src/client/containers/App.js @@ -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) } diff --git a/src/client/peer/__tests__/handshake-test.js b/src/client/peer/__tests__/handshake-test.js index 40971c1..5653313 100644 --- a/src/client/peer/__tests__/handshake-test.js +++ b/src/client/peer/__tests__/handshake-test.js @@ -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', diff --git a/src/client/peer/__tests__/peers-test.js b/src/client/peer/__tests__/peers-test.js index 4b61029..6885465 100644 --- a/src/client/peer/__tests__/peers-test.js +++ b/src/client/peer/__tests__/peers-test.js @@ -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"}' ]]) + }) + + }) }) diff --git a/src/client/peer/handshake.js b/src/client/peer/handshake.js index 4490553..9b560f4 100644 --- a/src/client/peer/handshake.js +++ b/src/client/peer/handshake.js @@ -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 }) } diff --git a/src/client/peer/peers.js b/src/client/peer/peers.js index 98e42df..dae567c 100644 --- a/src/client/peer/peers.js +++ b/src/client/peer/peers.js @@ -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] }) } diff --git a/src/client/reducers/__tests__/alerts-test.js b/src/client/reducers/__tests__/alerts-test.js new file mode 100644 index 0000000..8856285 --- /dev/null +++ b/src/client/reducers/__tests__/alerts-test.js @@ -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([]) + }) + + }) + +}) diff --git a/src/client/reducers/__tests__/streams-test.js b/src/client/reducers/__tests__/streams-test.js new file mode 100644 index 0000000..ce216fb --- /dev/null +++ b/src/client/reducers/__tests__/streams-test.js @@ -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: {} + }) + }) + }) + +}) + diff --git a/src/client/reducers/alerts.js b/src/client/reducers/alerts.js index 11ec04b..dcabd00 100644 --- a/src/client/reducers/alerts.js +++ b/src/client/reducers/alerts.js @@ -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: diff --git a/src/client/reducers/streams.js b/src/client/reducers/streams.js index 1ca8264..12ab7e9 100644 --- a/src/client/reducers/streams.js +++ b/src/client/reducers/streams.js @@ -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) { diff --git a/src/client/window/__mocks__/createObjectURL.js b/src/client/window/__mocks__/createObjectURL.js new file mode 100644 index 0000000..e876080 --- /dev/null +++ b/src/client/window/__mocks__/createObjectURL.js @@ -0,0 +1 @@ +export default object => 'blob://' + String(object) diff --git a/src/client/window/__mocks__/getUserMedia.js b/src/client/window/__mocks__/getUserMedia.js new file mode 100644 index 0000000..3526246 --- /dev/null +++ b/src/client/window/__mocks__/getUserMedia.js @@ -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')) +} +