First unit test
This commit is contained in:
parent
15e446b540
commit
33b891f170
4827
package-lock.json
generated
4827
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,7 @@
|
||||
"react-transition-group": "^1.1.3",
|
||||
"redux": "^3.6.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-promise-middleware": "^4.3.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"seamless-immutable": "^7.1.2",
|
||||
"simple-peer": "^8.1.0",
|
||||
@ -66,11 +67,14 @@
|
||||
"node-sass": "^4.5.3",
|
||||
"nodemon": "^1.11.0",
|
||||
"react-addons-test-utils": "^15.5.1",
|
||||
"redux-mock-store": "^1.2.3",
|
||||
"uglify-js": "^2.6.2",
|
||||
"watchify": "^3.9.0"
|
||||
},
|
||||
"jest": {
|
||||
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
|
||||
"transform": {
|
||||
".*": "<rootDir>/node_modules/babel-jest"
|
||||
},
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/node_modules/"
|
||||
]
|
||||
|
||||
1
src/client/__mocks__/callId.js
Normal file
1
src/client/__mocks__/callId.js
Normal file
@ -0,0 +1 @@
|
||||
export default 'call1234'
|
||||
46
src/client/__tests__/App-test.js
Normal file
46
src/client/__tests__/App-test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@ -6,15 +6,16 @@ import getUserMedia from '../window/getUserMedia.js'
|
||||
import handshake from '../peer/handshake.js'
|
||||
import socket from '../socket.js'
|
||||
|
||||
export const init = () => dispatch => {
|
||||
return Promise.all([
|
||||
export const init = () => dispatch => ({
|
||||
type: constants.INIT,
|
||||
payload: Promise.all([
|
||||
connect()(dispatch),
|
||||
getCameraStream()(dispatch)
|
||||
])
|
||||
.spread((socket, stream) => {
|
||||
handshake.init({ socket, callId, stream })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const connect = () => dispatch => {
|
||||
return new Promise(resolve => {
|
||||
|
||||
@ -1,33 +1,44 @@
|
||||
import * as constants from '../constants.js'
|
||||
|
||||
const TIMEOUT = 5000
|
||||
|
||||
function format (string, args) {
|
||||
string = args
|
||||
.reduce((string, arg, i) => string.replace('{' + i + '}', arg), string)
|
||||
return string
|
||||
}
|
||||
|
||||
function _notify (type, args) {
|
||||
const _notify = (type, args) => dispatch => {
|
||||
let string = args[0] || ''
|
||||
let message = format(string, Array.prototype.slice.call(args, 1))
|
||||
return {
|
||||
type: 'notify',
|
||||
payload: { type, message }
|
||||
}
|
||||
const payload = { type, message }
|
||||
dispatch({
|
||||
type: constants.NOTIFY,
|
||||
payload
|
||||
})
|
||||
setTimeout(() => {
|
||||
dispatch({
|
||||
type: constants.NOTIFY_DISMISS,
|
||||
payload
|
||||
})
|
||||
}, TIMEOUT)
|
||||
}
|
||||
|
||||
export function info () {
|
||||
return _notify('info', arguments)
|
||||
export const info = () => dispatch => {
|
||||
_notify('info', arguments)
|
||||
}
|
||||
|
||||
export function warn () {
|
||||
return _notify('warning', arguments)
|
||||
export const warn = () => dispatch => {
|
||||
_notify('warning', arguments)
|
||||
}
|
||||
|
||||
export function error () {
|
||||
return _notify('error', arguments)
|
||||
export const error = () => dispatch => {
|
||||
_notify('error', arguments)
|
||||
}
|
||||
|
||||
export function alert (message, dismissable) {
|
||||
return {
|
||||
type: 'alert',
|
||||
type: constants.ALERT,
|
||||
payload: {
|
||||
action: dismissable ? 'Dismiss' : '',
|
||||
dismissable: !!dismissable,
|
||||
@ -36,3 +47,10 @@ export function alert (message, dismissable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const dismiss = alert => {
|
||||
return {
|
||||
type: constants.ALERT_DISMISS,
|
||||
payload: alert
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ export const AlertPropType = PropTypes.shape({
|
||||
message: PropTypes.string.isRequired
|
||||
})
|
||||
|
||||
export class Alert extends React.PureComponent {
|
||||
export class Alert extends React.Component {
|
||||
static propTypes = {
|
||||
alert: AlertPropType,
|
||||
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 = {
|
||||
alerts: PropTypes.arrayOf(AlertPropType).isRequired,
|
||||
dismiss: PropTypes.func.isRequired
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import Alert from './Alerts.js'
|
||||
import Alerts, { AlertPropType } from './Alerts.js'
|
||||
import Input from './Input.js'
|
||||
import Notifications from './Notifications.js'
|
||||
import PropTypes from 'prop-types'
|
||||
@ -6,24 +6,28 @@ import React from 'react'
|
||||
import Video, { StreamPropType } from './Video.js'
|
||||
import _ from 'underscore'
|
||||
|
||||
export default class App extends React.PureComponent {
|
||||
export default class App extends React.Component {
|
||||
static propTypes = {
|
||||
streams: PropTypes.arrayOf(StreamPropType).isRequired,
|
||||
streams: PropTypes.objectOf(StreamPropType).isRequired,
|
||||
alerts: PropTypes.arrayOf(AlertPropType).isRequired,
|
||||
activate: PropTypes.func.isRequired,
|
||||
active: PropTypes.string.isRequired,
|
||||
init: PropTypes.func.isRequired
|
||||
active: PropTypes.string,
|
||||
init: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired
|
||||
}
|
||||
componentDidMount () {
|
||||
const { init } = this.props
|
||||
init()
|
||||
}
|
||||
render () {
|
||||
const { active, activate, streams } = this.props
|
||||
const {
|
||||
active, activate, alerts, dismiss, notify, notifications, streams
|
||||
} = this.props
|
||||
|
||||
return (<div className="app">
|
||||
<Alert />
|
||||
<Notifications />
|
||||
<Input />
|
||||
<Alerts alerts={alerts} dismiss={dismiss} />
|
||||
<Notifications notifications={notifications} />
|
||||
<Input notify={notify} />
|
||||
<div className="videos">
|
||||
{_.map(streams, (stream, userId) => (
|
||||
<Video
|
||||
|
||||
@ -2,7 +2,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import peers from '../peer/peers.js'
|
||||
|
||||
export default class Input extends React.PureComponent {
|
||||
export default class Input extends React.Component {
|
||||
static propTypes = {
|
||||
notify: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import CSSTransitionGroup from 'react-transition-group'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { CSSTransitionGroup } from 'react-transition-group'
|
||||
|
||||
export const NotificationPropTypes = PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
@ -9,7 +9,7 @@ export const NotificationPropTypes = PropTypes.shape({
|
||||
message: PropTypes.string.isRequired
|
||||
})
|
||||
|
||||
export default class Notifications extends React.PureComponent {
|
||||
export default class Notifications extends React.Component {
|
||||
static propTypes = {
|
||||
notifications: PropTypes.arrayOf(NotificationPropTypes).isRequired,
|
||||
max: PropTypes.number.isRequired
|
||||
|
||||
@ -9,7 +9,7 @@ export const StreamPropType = PropTypes.shape({
|
||||
url: PropTypes.string.isRequired
|
||||
})
|
||||
|
||||
export default class Video extends React.PureComponent {
|
||||
export default class Video extends React.Component {
|
||||
static propTypes = {
|
||||
activate: PropTypes.func.isRequired,
|
||||
active: PropTypes.string.required,
|
||||
|
||||
@ -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
|
||||
}]])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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'
|
||||
}]])
|
||||
})
|
||||
})
|
||||
@ -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 ]])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,5 +1,11 @@
|
||||
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 ALERT = 'ALERT'
|
||||
export const ALERT_DISMISS = 'ALERT_DISMISS'
|
||||
export const ALERT_CLEAR = 'ALERT_CLEAR'
|
||||
|
||||
27
src/client/containers/App.js
Normal file
27
src/client/containers/App.js
Normal 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)
|
||||
@ -3,7 +3,7 @@ import Immutable from 'seamless-immutable'
|
||||
|
||||
const defaultState = Immutable([])
|
||||
|
||||
export default function alert (state = defaultState, action) {
|
||||
export default function alerts (state = defaultState, action) {
|
||||
switch (action && action.type) {
|
||||
case constants.ALERT:
|
||||
return Immutable(state.asMutable().push(action.payload))
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import alerts from './alerts.js'
|
||||
import notifications from './notifications.js'
|
||||
import streams from './streams.js'
|
||||
import { combineReducers } from 'redux'
|
||||
|
||||
export default {
|
||||
export default combineReducers({
|
||||
alerts,
|
||||
notifications,
|
||||
streams
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,24 +1,18 @@
|
||||
import * as constants from '../constants.js'
|
||||
import Immutable from 'seamless-immutable'
|
||||
|
||||
const defaultState = Immutable({
|
||||
notifications: []
|
||||
})
|
||||
const defaultState = Immutable([])
|
||||
|
||||
export default function notify (state = defaultState, action) {
|
||||
export default function notifications (state = defaultState, action) {
|
||||
switch (action && action.type) {
|
||||
case constants.NOTIFY:
|
||||
const notifications = state.notifications.asMutable()
|
||||
const notifications = state.asMutable()
|
||||
notifications.push(action.payload)
|
||||
return state.merge({
|
||||
notifications
|
||||
})
|
||||
return Immutable(notifications)
|
||||
case constants.NOTIFY_DISMISS:
|
||||
return state.merge({
|
||||
notifications: state.notifications.filter(n => n !== action.payload)
|
||||
})
|
||||
return state.filter(n => n !== action.payload)
|
||||
case constants.NOTIFY_CLEAR:
|
||||
return state.merge({ notifications: [] })
|
||||
return defaultState
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
||||
@ -4,12 +4,12 @@ import Immutable from 'seamless-immutable'
|
||||
|
||||
const defaultState = Immutable({
|
||||
active: null,
|
||||
streams: {}
|
||||
all: {}
|
||||
})
|
||||
|
||||
function addStream (state, action) {
|
||||
const { userId, stream } = action.payload
|
||||
const streams = state.streams.merge({
|
||||
const streams = state.all.merge({
|
||||
[userId]: {
|
||||
userId,
|
||||
stream,
|
||||
@ -20,11 +20,11 @@ function addStream (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 })
|
||||
}
|
||||
|
||||
export default function stream (state = defaultState, action) {
|
||||
export default function streams (state = defaultState, action) {
|
||||
switch (action && action.type) {
|
||||
case constants.STREAM_ADD:
|
||||
return addStream(state, action)
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
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 { applyMiddleware, createStore } from 'redux'
|
||||
|
||||
export const middlewares = [thunk, promiseMiddleware(), logger]
|
||||
|
||||
export default createStore(
|
||||
reducer,
|
||||
applyMiddleware(thunk, logger)
|
||||
reducers,
|
||||
applyMiddleware.apply(null, middlewares)
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user