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",
"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/"
]

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 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 => {

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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,

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 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'

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([])
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))

View File

@ -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
}
})

View File

@ -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
}

View File

@ -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)

View File

@ -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)
)