diff --git a/src/client/__tests__/App-test.js b/src/client/__tests__/App-test.js
index a6d5592..abc5ecd 100644
--- a/src/client/__tests__/App-test.js
+++ b/src/client/__tests__/App-test.js
@@ -2,6 +2,7 @@ jest.mock('../actions/CallActions.js')
jest.mock('../callId.js')
jest.mock('../iceServers.js')
+import * as constants from '../constants.js'
import App from '../containers/App.js'
import React from 'react'
import ReactDOM from 'react-dom'
@@ -14,9 +15,11 @@ import { middlewares } from '../store.js'
describe('App', () => {
+ const initAction = { type: 'INIT' }
+
let state
beforeEach(() => {
- init.mockReturnValue({ type: 'INIT' })
+ init.mockReturnValue(initAction)
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' }
+ }])
+ })
+ })
+
+ })
+
})
diff --git a/src/client/__tests__/store-test.js b/src/client/__tests__/store-test.js
new file mode 100644
index 0000000..c7328be
--- /dev/null
+++ b/src/client/__tests__/store-test.js
@@ -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()
+ })
+
+})
diff --git a/src/client/actions/CallActions.js b/src/client/actions/CallActions.js
index 4ef22f0..1aa1846 100644
--- a/src/client/actions/CallActions.js
+++ b/src/client/actions/CallActions.js
@@ -1,3 +1,4 @@
+import * as StreamActions from './StreamActions.js'
import * as NotifyActions from './NotifyActions.js'
import * as constants from '../constants.js'
import Promise from 'bluebird'
@@ -34,7 +35,7 @@ export const connect = () => dispatch => {
export const getCameraStream = () => dispatch => {
return getUserMedia({ video: true, audio: true })
.then(stream => {
- dispatch(addStream({ stream, userId: constants.ME }))
+ dispatch(StreamActions.addStream({ stream, userId: constants.ME }))
return stream
})
.catch(err => {
@@ -42,21 +43,3 @@ export const getCameraStream = () => dispatch => {
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 }
-})
diff --git a/src/client/actions/NotifyActions.js b/src/client/actions/NotifyActions.js
index 72ff674..43f4095 100644
--- a/src/client/actions/NotifyActions.js
+++ b/src/client/actions/NotifyActions.js
@@ -1,5 +1,5 @@
import * as constants from '../constants.js'
-import Immutable from 'seamless-immutable'
+import _ from 'underscore'
const TIMEOUT = 5000
@@ -12,7 +12,8 @@ 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 = Immutable({ type, message })
+ const id = _.uniqueId('notification')
+ const payload = { id, type, message }
dispatch({
type: constants.NOTIFY,
payload
@@ -20,7 +21,7 @@ const _notify = (type, args) => dispatch => {
setTimeout(() => {
dispatch({
type: constants.NOTIFY_DISMISS,
- payload
+ payload: { id }
})
}, TIMEOUT)
}
diff --git a/src/client/actions/StreamActions.js b/src/client/actions/StreamActions.js
new file mode 100644
index 0000000..2499448
--- /dev/null
+++ b/src/client/actions/StreamActions.js
@@ -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 }
+})
diff --git a/src/client/actions/__tests__/CallActions-test.js b/src/client/actions/__tests__/CallActions-test.js
index 3e7b865..a10373f 100644
--- a/src/client/actions/__tests__/CallActions-test.js
+++ b/src/client/actions/__tests__/CallActions-test.js
@@ -38,6 +38,7 @@ describe('reducers/alerts', () => {
}, {
type: constants.NOTIFY,
payload: {
+ id: jasmine.any(String),
message: 'Connected to server socket',
type: 'warning'
}
@@ -62,12 +63,14 @@ describe('reducers/alerts', () => {
}, {
type: constants.NOTIFY,
payload: {
+ id: jasmine.any(String),
message: 'Connected to server socket',
type: 'warning'
}
}, {
type: constants.NOTIFY,
payload: {
+ id: jasmine.any(String),
message: 'Server socket disconnected',
type: 'error'
}
diff --git a/src/client/components/Alerts.js b/src/client/components/Alerts.js
index 685065b..78a6d8a 100644
--- a/src/client/components/Alerts.js
+++ b/src/client/components/Alerts.js
@@ -18,13 +18,16 @@ export class Alert extends React.Component {
dismiss(alert)
}
render () {
- const { alert, dismiss } = this.props
+ const { alert } = this.props
return (
{alert.message}
{alert.dismissable && (
-
+
)}
)
diff --git a/src/client/components/App.js b/src/client/components/App.js
index 86f14f0..81a2169 100644
--- a/src/client/components/App.js
+++ b/src/client/components/App.js
@@ -34,6 +34,7 @@ export default class App extends React.Component {
))}
diff --git a/src/client/components/Notifications.js b/src/client/components/Notifications.js
index 68af853..5cf07bf 100644
--- a/src/client/components/Notifications.js
+++ b/src/client/components/Notifications.js
@@ -11,7 +11,7 @@ export const NotificationPropTypes = PropTypes.shape({
export default class Notifications extends React.Component {
static propTypes = {
- notifications: PropTypes.arrayOf(NotificationPropTypes).isRequired,
+ notifications: PropTypes.objectOf(NotificationPropTypes).isRequired,
max: PropTypes.number.isRequired
}
static defaultProps = {
@@ -26,12 +26,12 @@ export default class Notifications extends React.Component {
transitionLeaveTimeout={100}
transitionName="fade"
>
- {notifications.slice(max).map(notification => (
+ {Object.keys(notifications).slice(-max).map(id => (
- {notification.message}
+ {notifications[id].message}
))}
diff --git a/src/client/components/Video.js b/src/client/components/Video.js
index a1010a9..82c099d 100644
--- a/src/client/components/Video.js
+++ b/src/client/components/Video.js
@@ -5,14 +5,13 @@ import { ME } from '../constants.js'
export const StreamPropType = PropTypes.shape({
userId: PropTypes.string.isRequired,
- stream: PropTypes.instanceOf(ArrayBuffer).isRequired,
url: PropTypes.string.isRequired
})
export default class Video extends React.Component {
static propTypes = {
activate: PropTypes.func.isRequired,
- active: PropTypes.string.required,
+ active: PropTypes.bool.isRequired,
stream: StreamPropType.isRequired
}
activate = e => {
diff --git a/src/client/containers/App.js b/src/client/containers/App.js
index 73daeea..fc24e32 100644
--- a/src/client/containers/App.js
+++ b/src/client/containers/App.js
@@ -1,5 +1,6 @@
-import * as NotifyActions from '../actions/NotifyActions.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 React from 'react'
import { bindActionCreators } from 'redux'
@@ -17,7 +18,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
- activate: bindActionCreators(CallActions.activateStream, dispatch),
+ activate: bindActionCreators(StreamActions.activateStream, 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 5653313..5847aa4 100644
--- a/src/client/peer/__tests__/handshake-test.js
+++ b/src/client/peer/__tests__/handshake-test.js
@@ -145,6 +145,7 @@ describe('handshake', () => {
expect(store.getActions()).toEqual([{
type: constants.NOTIFY,
payload: {
+ id: jasmine.any(String),
message: 'Peer connection closed',
type: 'error'
}
diff --git a/src/client/peer/__tests__/peers-test.js b/src/client/peer/__tests__/peers-test.js
index 6885465..450c099 100644
--- a/src/client/peer/__tests__/peers-test.js
+++ b/src/client/peer/__tests__/peers-test.js
@@ -49,6 +49,7 @@ describe('peers', () => {
connecting: {
type: constants.NOTIFY,
payload: {
+ id: jasmine.any(String),
message: 'Connecting to peer...',
type: 'warning'
}
@@ -56,6 +57,7 @@ describe('peers', () => {
established: {
type: constants.NOTIFY,
payload: {
+ id: jasmine.any(String),
message: 'Peer connection established',
type: 'warning'
}
@@ -140,6 +142,7 @@ describe('peers', () => {
expect(store.getActions()).toEqual([{
type: constants.NOTIFY,
payload: {
+ id: jasmine.any(String),
type: 'info',
message: `${user.id}: ${message}`
}
diff --git a/src/client/peer/peers.js b/src/client/peer/peers.js
index dae567c..8b444a4 100644
--- a/src/client/peer/peers.js
+++ b/src/client/peer/peers.js
@@ -1,5 +1,6 @@
import * as CallActions from '../actions/CallActions.js'
import * as NotifyActions from '../actions/NotifyActions.js'
+import * as StreamActions from '../actions/StreamActions.js'
import Peer from 'simple-peer'
import _ from 'underscore'
import _debug from 'debug'
@@ -63,7 +64,7 @@ function create ({ socket, user, initiator, stream }) {
peer.on('stream', stream => {
debug('peer: %s, stream', user.id)
- dispatch(CallActions.addStream({
+ dispatch(StreamActions.addStream({
userId: user.id,
stream
}))
@@ -83,7 +84,7 @@ function create ({ socket, user, initiator, stream }) {
NotifyActions.error('Peer connection closed')
)
dispatch(
- CallActions.removeStream(user.id)
+ StreamActions.removeStream(user.id)
)
delete peers[user.id]
diff --git a/src/client/reducers/__tests__/alerts-test.js b/src/client/reducers/__tests__/alerts-test.js
index 8856285..8432d07 100644
--- a/src/client/reducers/__tests__/alerts-test.js
+++ b/src/client/reducers/__tests__/alerts-test.js
@@ -1,4 +1,5 @@
import * as NotifyActions from '../../actions/NotifyActions.js'
+import _ from 'underscore'
import { applyMiddleware, createStore } from 'redux'
import { create } from '../../middlewares.js'
import reducers from '../index.js'
@@ -65,7 +66,8 @@ describe('reducers/alerts', () => {
})
it('adds a notification', () => {
- expect(store.getState().notifications).toEqual([{
+ expect(_.values(store.getState().notifications)).toEqual([{
+ id: jasmine.any(String),
message: 'Hi John!',
type
}])
@@ -73,9 +75,14 @@ describe('reducers/alerts', () => {
it('dismisses notification after a timeout', () => {
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.error('Hi {0}!', 'John'))
store.dispatch(NotifyActions.clear())
- expect(store.getState().notifications).toEqual([])
+ expect(store.getState().notifications).toEqual({})
})
})
diff --git a/src/client/reducers/__tests__/streams-test.js b/src/client/reducers/__tests__/streams-test.js
index ce216fb..7ad5563 100644
--- a/src/client/reducers/__tests__/streams-test.js
+++ b/src/client/reducers/__tests__/streams-test.js
@@ -2,7 +2,7 @@ jest.mock('../../callId.js')
jest.mock('../../iceServers.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 { create } from '../../middlewares.js'
import reducers from '../index.js'
@@ -31,13 +31,12 @@ describe('reducers/alerts', () => {
describe('addStream', () => {
it('adds a stream', () => {
- store.dispatch(CallActions.addStream({ userId, stream }))
+ store.dispatch(StreamActions.addStream({ userId, stream }))
expect(store.getState().streams).toEqual({
active: userId,
all: {
[userId]: {
userId,
- stream,
url: jasmine.any(String)
}
}
@@ -47,8 +46,8 @@ describe('reducers/alerts', () => {
describe('removeStream', () => {
it('removes a stream', () => {
- store.dispatch(CallActions.addStream({ userId, stream }))
- store.dispatch(CallActions.removeStream(userId))
+ store.dispatch(StreamActions.addStream({ userId, stream }))
+ store.dispatch(StreamActions.removeStream(userId))
expect(store.getState().streams).toEqual({
active: userId,
all: {}
@@ -58,7 +57,7 @@ describe('reducers/alerts', () => {
describe('activateStream', () => {
it('activates a stream', () => {
- store.dispatch(CallActions.activateStream(userId))
+ store.dispatch(StreamActions.activateStream(userId))
expect(store.getState().streams).toEqual({
active: userId,
all: {}
diff --git a/src/client/reducers/notifications.js b/src/client/reducers/notifications.js
index d96a1fb..0ad94cf 100644
--- a/src/client/reducers/notifications.js
+++ b/src/client/reducers/notifications.js
@@ -1,16 +1,16 @@
import * as constants from '../constants.js'
import Immutable from 'seamless-immutable'
-const defaultState = Immutable([])
+const defaultState = Immutable({})
export default function notifications (state = defaultState, action) {
switch (action && action.type) {
case constants.NOTIFY:
- const notifications = state.asMutable()
- notifications.push(action.payload)
- return Immutable(notifications)
+ return state.merge({
+ [action.payload.id]: action.payload
+ })
case constants.NOTIFY_DISMISS:
- return state.filter(n => n !== action.payload)
+ return state.without(action.payload.id)
case constants.NOTIFY_CLEAR:
return defaultState
default:
diff --git a/src/client/reducers/streams.js b/src/client/reducers/streams.js
index 12ab7e9..c29c54d 100644
--- a/src/client/reducers/streams.js
+++ b/src/client/reducers/streams.js
@@ -12,7 +12,6 @@ function addStream (state, action) {
const all = state.all.merge({
[userId]: {
userId,
- stream,
url: createObjectURL(stream)
}
})
diff --git a/src/client/store.js b/src/client/store.js
index e5a4b21..74d3283 100644
--- a/src/client/store.js
+++ b/src/client/store.js
@@ -1,7 +1,6 @@
import { create } from './middlewares.js'
import reducers from './reducers'
import { applyMiddleware, createStore } from 'redux'
-
export const middlewares = create(
window.localStorage && window.localStorage.debug
)