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'))
+}
+