First unit test

This commit is contained in:
Jerko Steiner 2017-06-14 21:01:27 -04:00
parent 15e446b540
commit 33b891f170
21 changed files with 158 additions and 5067 deletions

4827
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,7 @@
"react-transition-group": "^1.1.3", "react-transition-group": "^1.1.3",
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-promise-middleware": "^4.3.0",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
"seamless-immutable": "^7.1.2", "seamless-immutable": "^7.1.2",
"simple-peer": "^8.1.0", "simple-peer": "^8.1.0",
@ -66,11 +67,14 @@
"node-sass": "^4.5.3", "node-sass": "^4.5.3",
"nodemon": "^1.11.0", "nodemon": "^1.11.0",
"react-addons-test-utils": "^15.5.1", "react-addons-test-utils": "^15.5.1",
"redux-mock-store": "^1.2.3",
"uglify-js": "^2.6.2", "uglify-js": "^2.6.2",
"watchify": "^3.9.0" "watchify": "^3.9.0"
}, },
"jest": { "jest": {
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest", "transform": {
".*": "<rootDir>/node_modules/babel-jest"
},
"modulePathIgnorePatterns": [ "modulePathIgnorePatterns": [
"<rootDir>/node_modules/" "<rootDir>/node_modules/"
] ]

View File

@ -0,0 +1 @@
export default 'call1234'

View File

@ -0,0 +1,46 @@
jest.mock('../actions/CallActions.js')
jest.mock('../callId.js')
jest.mock('../iceServers.js')
import App from '../containers/App.js'
import React from 'react'
import ReactDOM from 'react-dom'
import TestUtils from 'react-dom/test-utils'
import configureStore from 'redux-mock-store'
import reducers from '../reducers'
import { Provider } from 'react-redux'
import { init } from '../actions/CallActions.js'
import { middlewares } from '../store.js'
// jest.useFakeTimers()
describe('App', () => {
let state
beforeEach(() => {
init.mockReturnValue({ type: 'INIT' })
state = reducers()
})
let component, node, store
function render() {
store = configureStore(middlewares)(state)
console.log(store.getState())
component = TestUtils.renderIntoDocument(
<Provider store={store}>
<App />
</Provider>
)
node = ReactDOM.findDOMNode(component)
}
describe('render', () => {
it('renders without issues', () => {
render()
// jest.runAllTimers()
expect(node).toBeTruthy()
expect(init.mock.calls.length).toBe(1)
})
})
})

View File

@ -6,15 +6,16 @@ import getUserMedia from '../window/getUserMedia.js'
import handshake from '../peer/handshake.js' import handshake from '../peer/handshake.js'
import socket from '../socket.js' import socket from '../socket.js'
export const init = () => dispatch => { export const init = () => dispatch => ({
return Promise.all([ type: constants.INIT,
payload: Promise.all([
connect()(dispatch), connect()(dispatch),
getCameraStream()(dispatch) getCameraStream()(dispatch)
]) ])
.spread((socket, stream) => { .spread((socket, stream) => {
handshake.init({ socket, callId, stream }) handshake.init({ socket, callId, stream })
}) })
} })
export const connect = () => dispatch => { export const connect = () => dispatch => {
return new Promise(resolve => { return new Promise(resolve => {

View File

@ -1,33 +1,44 @@
import * as constants from '../constants.js'
const TIMEOUT = 5000
function format (string, args) { function format (string, args) {
string = args string = args
.reduce((string, arg, i) => string.replace('{' + i + '}', arg), string) .reduce((string, arg, i) => string.replace('{' + i + '}', arg), string)
return string return string
} }
function _notify (type, args) { 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))
return { const payload = { type, message }
type: 'notify', dispatch({
payload: { type, message } type: constants.NOTIFY,
} payload
})
setTimeout(() => {
dispatch({
type: constants.NOTIFY_DISMISS,
payload
})
}, TIMEOUT)
} }
export function info () { export const info = () => dispatch => {
return _notify('info', arguments) _notify('info', arguments)
} }
export function warn () { export const warn = () => dispatch => {
return _notify('warning', arguments) _notify('warning', arguments)
} }
export function error () { export const error = () => dispatch => {
return _notify('error', arguments) _notify('error', arguments)
} }
export function alert (message, dismissable) { export function alert (message, dismissable) {
return { return {
type: 'alert', type: constants.ALERT,
payload: { payload: {
action: dismissable ? 'Dismiss' : '', action: dismissable ? 'Dismiss' : '',
dismissable: !!dismissable, dismissable: !!dismissable,
@ -36,3 +47,10 @@ export function alert (message, dismissable) {
} }
} }
} }
export const dismiss = alert => {
return {
type: constants.ALERT_DISMISS,
payload: alert
}
}

View File

@ -8,7 +8,7 @@ export const AlertPropType = PropTypes.shape({
message: PropTypes.string.isRequired message: PropTypes.string.isRequired
}) })
export class Alert extends React.PureComponent { export class Alert extends React.Component {
static propTypes = { static propTypes = {
alert: AlertPropType, alert: AlertPropType,
dismiss: PropTypes.func.isRequired dismiss: PropTypes.func.isRequired
@ -31,7 +31,7 @@ export class Alert extends React.PureComponent {
} }
} }
export default class Alerts extends React.PureComponent { export default class Alerts extends React.Component {
static propTypes = { static propTypes = {
alerts: PropTypes.arrayOf(AlertPropType).isRequired, alerts: PropTypes.arrayOf(AlertPropType).isRequired,
dismiss: PropTypes.func.isRequired dismiss: PropTypes.func.isRequired

View File

@ -1,4 +1,4 @@
import Alert from './Alerts.js' import Alerts, { AlertPropType } from './Alerts.js'
import Input from './Input.js' import Input from './Input.js'
import Notifications from './Notifications.js' import Notifications from './Notifications.js'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
@ -6,24 +6,28 @@ import React from 'react'
import Video, { StreamPropType } from './Video.js' import Video, { StreamPropType } from './Video.js'
import _ from 'underscore' import _ from 'underscore'
export default class App extends React.PureComponent { export default class App extends React.Component {
static propTypes = { static propTypes = {
streams: PropTypes.arrayOf(StreamPropType).isRequired, streams: PropTypes.objectOf(StreamPropType).isRequired,
alerts: PropTypes.arrayOf(AlertPropType).isRequired,
activate: PropTypes.func.isRequired, activate: PropTypes.func.isRequired,
active: PropTypes.string.isRequired, active: PropTypes.string,
init: PropTypes.func.isRequired init: PropTypes.func.isRequired,
notify: PropTypes.func.isRequired
} }
componentDidMount () { componentDidMount () {
const { init } = this.props const { init } = this.props
init() init()
} }
render () { render () {
const { active, activate, streams } = this.props const {
active, activate, alerts, dismiss, notify, notifications, streams
} = this.props
return (<div className="app"> return (<div className="app">
<Alert /> <Alerts alerts={alerts} dismiss={dismiss} />
<Notifications /> <Notifications notifications={notifications} />
<Input /> <Input notify={notify} />
<div className="videos"> <div className="videos">
{_.map(streams, (stream, userId) => ( {_.map(streams, (stream, userId) => (
<Video <Video

View File

@ -2,7 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import peers from '../peer/peers.js' import peers from '../peer/peers.js'
export default class Input extends React.PureComponent { export default class Input extends React.Component {
static propTypes = { static propTypes = {
notify: PropTypes.func.isRequired notify: PropTypes.func.isRequired
} }

View File

@ -1,7 +1,7 @@
import CSSTransitionGroup from 'react-transition-group'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import { CSSTransitionGroup } from 'react-transition-group'
export const NotificationPropTypes = PropTypes.shape({ export const NotificationPropTypes = PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
@ -9,7 +9,7 @@ export const NotificationPropTypes = PropTypes.shape({
message: PropTypes.string.isRequired message: PropTypes.string.isRequired
}) })
export default class Notifications extends React.PureComponent { export default class Notifications extends React.Component {
static propTypes = { static propTypes = {
notifications: PropTypes.arrayOf(NotificationPropTypes).isRequired, notifications: PropTypes.arrayOf(NotificationPropTypes).isRequired,
max: PropTypes.number.isRequired max: PropTypes.number.isRequired

View File

@ -9,7 +9,7 @@ export const StreamPropType = PropTypes.shape({
url: PropTypes.string.isRequired url: PropTypes.string.isRequired
}) })
export default class Video extends React.PureComponent { 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.string.required,

View File

@ -1,71 +0,0 @@
jest.unmock('../alert.js')
const React = require('react')
const ReactDOM = require('react-dom')
const TestUtils = require('react-addons-test-utils')
const Alert = require('../alert.js')
const dispatcher = require('../../dispatcher/dispatcher.js')
const alertStore = require('../../store/alertStore.js')
describe('alert', () => {
beforeEach(() => {
alertStore.getAlert.mockClear()
})
function render () {
let component = TestUtils.renderIntoDocument(<div><Alert /></div>)
return ReactDOM.findDOMNode(component)
}
describe('render', () => {
it('should do nothing when no alert', () => {
let node = render()
expect(node.querySelector('.alert.hidden')).toBeTruthy()
})
it('should render alert', () => {
alertStore.getAlert.mockReturnValue({
message: 'example',
type: 'warning'
})
let node = render()
expect(node.querySelector('.alert.warning')).toBeTruthy()
expect(node.querySelector('.alert span').textContent).toMatch(/example/)
expect(node.querySelector('.alert button')).toBeNull()
})
it('should render dismissable alert', () => {
alertStore.getAlert.mockReturnValue({
message: 'example',
type: 'warning',
dismissable: true
})
let node = render()
expect(node.querySelector('.alert.warning')).toBeTruthy()
expect(node.querySelector('.alert span').textContent).toMatch(/example/)
expect(node.querySelector('.alert button')).toBeTruthy()
})
it('should dispatch dismiss alert on dismiss clicked', () => {
let alert = {
message: 'example',
type: 'warning',
dismissable: true
}
alertStore.getAlert.mockReturnValue(alert)
let node = render()
TestUtils.Simulate.click(node.querySelector('.alert button'))
expect(dispatcher.dispatch.mock.calls).toEqual([[{
type: 'alert-dismiss',
alert
}]])
})
})
})

View File

@ -1,60 +0,0 @@
jest.unmock('../app.js')
jest.unmock('underscore')
const React = require('react')
const ReactDOM = require('react-dom')
const TestUtils = require('react-addons-test-utils')
require('../alert.js').mockImplementation(() => <div />)
require('../notifications.js').mockImplementation(() => <div />)
const App = require('../app.js')
const activeStore = require('../../store/activeStore.js')
const dispatcher = require('../../dispatcher/dispatcher.js')
const streamStore = require('../../store/streamStore.js')
describe('app', () => {
beforeEach(() => {
dispatcher.dispatch.mockClear()
})
function render (active) {
streamStore.getStreams.mockReturnValue({
user1: { stream: 1 },
user2: { stream: 2 }
})
let component = TestUtils.renderIntoDocument(<div><App /></div>)
return ReactDOM.findDOMNode(component).children[0]
}
it('should render div.app', () => {
let node = render()
expect(node.tagName).toBe('DIV')
expect(node.className).toBe('app')
})
it('should have rendered two videos', () => {
let node = render()
expect(node.querySelectorAll('video').length).toBe(2)
})
it('should mark .active video', () => {
activeStore.getActive.mockReturnValue('user1')
activeStore.isActive.mockImplementation(test => test === 'user1')
let node = render()
expect(node.querySelectorAll('.video-container').length).toBe(2)
expect(node.querySelectorAll('.video-container.active').length).toBe(1)
})
it('should dispatch mark-active on video click', () => {
let node = render()
TestUtils.Simulate.click(node.querySelectorAll('video')[1])
expect(dispatcher.dispatch.mock.calls).toEqual([[{
type: 'mark-active',
userId: 'user2'
}]])
})
})

View File

@ -1,56 +0,0 @@
jest.unmock('../notifications.js')
const React = require('react')
const ReactDOM = require('react-dom')
const TestUtils = require('react-addons-test-utils')
const Notifications = require('../notifications.js')
const notificationsStore = require('../../store/notificationsStore.js')
describe('alert', () => {
beforeEach(() => {
notificationsStore.getNotifications.mockClear()
notificationsStore.getNotifications.mockReturnValue([])
})
function render (component) {
let rendered = TestUtils.renderIntoDocument(<div>{component}</div>)
return ReactDOM.findDOMNode(rendered)
}
describe('render', () => {
it('should render notifications placeholder', () => {
let node = render(<Notifications />)
expect(node.querySelector('.notifications')).toBeTruthy()
expect(node.querySelector('.notifications .notification')).toBeFalsy()
})
it('should render notifications', () => {
notificationsStore.getNotifications.mockReturnValue([{
_id: 1,
message: 'message 1',
type: 'warning'
}, {
_id: 2,
message: 'message 2',
type: 'error'
}])
let node = render(<Notifications />)
expect(notificationsStore.getNotifications.mock.calls).toEqual([[ 10 ]])
let c = node.querySelector('.notifications')
expect(c).toBeTruthy()
expect(c.querySelectorAll('.notification').length).toBe(2)
expect(c.querySelector('.notification.warning').textContent)
.toEqual('message 1')
expect(c.querySelector('.notification.error').textContent)
.toEqual('message 2')
})
it('should render max X notifications', () => {
render(<Notifications max={1} />)
expect(notificationsStore.getNotifications.mock.calls).toEqual([[ 1 ]])
})
})
})

View File

@ -1,5 +1,11 @@
import { PENDING, FULFILLED, REJECTED } from 'redux-promise-middleware'
export const ME = '_me_' 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 ALERT = 'ALERT' export const ALERT = 'ALERT'
export const ALERT_DISMISS = 'ALERT_DISMISS' export const ALERT_DISMISS = 'ALERT_DISMISS'
export const ALERT_CLEAR = 'ALERT_CLEAR' export const ALERT_CLEAR = 'ALERT_CLEAR'

View File

@ -0,0 +1,27 @@
import * as NotifyActions from '../actions/NotifyActions.js'
import * as CallActions from '../actions/CallActions.js'
import App from '../components/App.js'
import React from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import peers from '../peer/peers.js'
function mapStateToProps(state) {
return {
streams: state.streams.all,
alerts: state.alerts,
notifications: state.notifications,
active: state.streams.active
}
}
function mapDispatchToProps(dispatch) {
return {
activate: bindActionCreators(CallActions.activateStream, dispatch),
dismiss: bindActionCreators(NotifyActions.dismiss, dispatch),
init: bindActionCreators(CallActions.init, dispatch),
notify: bindActionCreators(NotifyActions.info, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App)

View File

@ -3,7 +3,7 @@ import Immutable from 'seamless-immutable'
const defaultState = Immutable([]) const defaultState = Immutable([])
export default function alert (state = defaultState, action) { export default function alerts (state = defaultState, action) {
switch (action && action.type) { switch (action && action.type) {
case constants.ALERT: case constants.ALERT:
return Immutable(state.asMutable().push(action.payload)) return Immutable(state.asMutable().push(action.payload))

View File

@ -1,9 +1,10 @@
import alerts from './alerts.js' import alerts from './alerts.js'
import notifications from './notifications.js' import notifications from './notifications.js'
import streams from './streams.js' import streams from './streams.js'
import { combineReducers } from 'redux'
export default { export default combineReducers({
alerts, alerts,
notifications, notifications,
streams streams
} })

View File

@ -1,24 +1,18 @@
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([])
notifications: []
})
export default function notify (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.notifications.asMutable() const notifications = state.asMutable()
notifications.push(action.payload) notifications.push(action.payload)
return state.merge({ return Immutable(notifications)
notifications
})
case constants.NOTIFY_DISMISS: case constants.NOTIFY_DISMISS:
return state.merge({ return state.filter(n => n !== action.payload)
notifications: state.notifications.filter(n => n !== action.payload)
})
case constants.NOTIFY_CLEAR: case constants.NOTIFY_CLEAR:
return state.merge({ notifications: [] }) return defaultState
default: default:
return state return state
} }

View File

@ -4,12 +4,12 @@ import Immutable from 'seamless-immutable'
const defaultState = Immutable({ const defaultState = Immutable({
active: null, active: null,
streams: {} all: {}
}) })
function addStream (state, action) { function addStream (state, action) {
const { userId, stream } = action.payload const { userId, stream } = action.payload
const streams = state.streams.merge({ const streams = state.all.merge({
[userId]: { [userId]: {
userId, userId,
stream, stream,
@ -20,11 +20,11 @@ function addStream (state, action) {
} }
function removeStream (state, action) { function removeStream (state, action) {
const streams = state.streams.without(action.payload.userId) const streams = state.all.without(action.payload.userId)
return state.merge({ streams }) return state.merge({ streams })
} }
export default function stream (state = defaultState, action) { export default function streams (state = defaultState, action) {
switch (action && action.type) { switch (action && action.type) {
case constants.STREAM_ADD: case constants.STREAM_ADD:
return addStream(state, action) return addStream(state, action)

View File

@ -1,9 +1,12 @@
import logger from 'redux-logger' import logger from 'redux-logger'
import reducer from './reducers' import promiseMiddleware from 'redux-promise-middleware'
import reducers from './reducers'
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
import { applyMiddleware, createStore } from 'redux' import { applyMiddleware, createStore } from 'redux'
export const middlewares = [thunk, promiseMiddleware(), logger]
export default createStore( export default createStore(
reducer, reducers,
applyMiddleware(thunk, logger) applyMiddleware.apply(null, middlewares)
) )