diff --git a/.gitignore b/.gitignore index 598afce..b6f8f7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules src/dist/public -src/server/logs \ No newline at end of file +src/server/logs +logs \ No newline at end of file diff --git a/config/library.js b/config/library.js index 613e265..464e0ba 100644 --- a/config/library.js +++ b/config/library.js @@ -2,5 +2,5 @@ require('react'); require('react-dom'); require('react-redux'); require('redux'); -require('react-player'); -require('redux-saga'); \ No newline at end of file +require('redux-saga'); +require('react-bootstrap'); diff --git a/package.json b/package.json index 4998dd0..b73b77b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "babel-eslint": "^8.2.6", "babel-loader": "^8.0.0-beta.4", "babel-polyfill": "^6.26.0", + "chai": "^4.1.2", + "chai-enzyme": "^1.0.0-beta.1", "chromedriver": "^2.41.0", "css-loader": "^1.0.0", "cucumber": "^4.2.1", @@ -43,6 +45,7 @@ "express": "^4.16.3", "extract-text-webpack-plugin": "^4.0.0-beta.0", "jsdom": "^12.0.0", + "jsdom-global": "^3.0.2", "mocha": "^5.2.0", "nightwatch": "^0.9.21", "nightwatch-cucumber": "^9.1.2", @@ -54,17 +57,18 @@ "style-loader": "^0.22.1", "stylelint": "^9.5.0", "stylelint-config-standard": "^18.2.0", + "testdouble": "^3.8.1", "uglifyjs-webpack-plugin": "^1.3.0", "webpack": "^4.17.0", "webpack-cli": "^3.1.0" }, "dependencies": { - "react-player": "^1.6.4", - "redux-saga": "^0.16.0", "react": "^16.4.2", + "react-bootstrap": "^0.32.3", "react-dom": "^16.4.2", "react-redux": "^5.0.7", - "redux": "^4.0.0" + "redux": "^4.0.0", + "redux-saga": "^0.16.0" }, "nyc": { "extension": [ diff --git a/src/client/actions/index.js b/src/client/actions/index.js index b89ccf6..e5b0622 100644 --- a/src/client/actions/index.js +++ b/src/client/actions/index.js @@ -1,4 +1,8 @@ import { action } from '../helpers/actionCreator'; import * as actions from '../constants/index'; -export const add = text => action(actions.ADD, { text }); +export const add = number => action(actions.ADD, { number }); + +export const sub = number => action(actions.SUB, { number }); + +export const change = number => action(actions.CHANGE, { number }); diff --git a/src/client/components/App.jsx b/src/client/components/App.jsx index 941e7e1..24dde61 100644 --- a/src/client/components/App.jsx +++ b/src/client/components/App.jsx @@ -1,35 +1,37 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ReactPlayer from 'react-player'; + import '../styles/App.scss'; +import { Button, FormControl } from 'react-bootstrap'; export default class App extends React.Component { constructor(props) { super(props); + this.handleChange = this.handleChange.bind(this); + } + handleChange(e) { + parseInt(e.target.value) ? this.props.change(parseInt(e.target.value)): ''; } render() { - return this.props.number == 1 ? ( -
- -
- ) : ( -
- {' '} - Welcome{' '} - this.props.add(1)} - />{' '} -
+ return (
+ + + +
); } } App.propTypes = { - number: PropTypes.string, + number: PropTypes.number, add: PropTypes.func, - recipes: PropTypes.array, + sub: PropTypes.func, + change: PropTypes.func, }; diff --git a/src/client/constants/index.js b/src/client/constants/index.js index d319394..ac819ba 100644 --- a/src/client/constants/index.js +++ b/src/client/constants/index.js @@ -1,4 +1,5 @@ +export const INITALIZE_APPLICATION = 'INITALIZE_APPLICATION'; +export const CHANGE = 'CHANGE'; export const ADD = 'ADD'; +export const SUB = 'SUB'; export const SAVE = 'SAVE'; -export const START_APPLICATION = 'START_APPLICATION'; -export const ADD_RECIPES = 'ADD_RECIPES'; diff --git a/src/client/containers/App.js b/src/client/containers/App.js index dac1c0e..aaf0ee6 100644 --- a/src/client/containers/App.js +++ b/src/client/containers/App.js @@ -1,17 +1,18 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import App from '../components/App'; -import { add } from '../actions'; +import { add, sub, change } from '../actions'; const mapStateToProps = state => ({ - number: state.add.number, - recipes: state.recipe.recipes, + number: state.calc.number, }); const mapDispatchToProps = dispatch => bindActionCreators( { add, + sub, + change, }, dispatch ); diff --git a/src/client/index.js b/src/client/index.js index 02be6ce..dc5da95 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -1,18 +1,19 @@ import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; -import { createStore, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware, compose } from 'redux'; import createSagaMiddleware from 'redux-saga'; +const sagaMiddleware = createSagaMiddleware(); import cr from './reducers/combined'; import AppContainer from './containers/App'; import rootSaga from './sagas/combined'; -const sagaMiddleware = createSagaMiddleware(); +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; import * as actions from './constants'; // then run the saga -const store = createStore(cr, applyMiddleware(sagaMiddleware)); +const store = createStore(cr, composeEnhancers(applyMiddleware(sagaMiddleware))); sagaMiddleware.run(rootSaga); export const action = type => store.dispatch({ type }); -action(actions.START_APPLICATION); +action(actions.INITALIZE_APPLICATION); render( diff --git a/src/client/reducers/add.js b/src/client/reducers/calc.js similarity index 76% rename from src/client/reducers/add.js rename to src/client/reducers/calc.js index ab38ca9..c74b879 100644 --- a/src/client/reducers/add.js +++ b/src/client/reducers/calc.js @@ -1,6 +1,6 @@ import * as actions from '../constants'; -const add = (state = {}, action) => { +const calc = (state = {}, action) => { switch (action.type) { case actions.SAVE: return Object.assign({}, state, { @@ -11,4 +11,4 @@ const add = (state = {}, action) => { } }; -export default add; +export default calc; diff --git a/src/client/reducers/combined.js b/src/client/reducers/combined.js index 9e41f0f..e2f8b61 100644 --- a/src/client/reducers/combined.js +++ b/src/client/reducers/combined.js @@ -1,10 +1,8 @@ import { combineReducers } from 'redux'; -import add from './add'; -import recipe from './recipe'; +import calc from './calc'; const combined = combineReducers({ - add, - recipe, + calc, }); export default combined; diff --git a/src/client/reducers/recipe.js b/src/client/reducers/recipe.js deleted file mode 100644 index 0b04ee1..0000000 --- a/src/client/reducers/recipe.js +++ /dev/null @@ -1,14 +0,0 @@ -import * as actions from '../constants'; - -const recipe = (state = {}, action) => { - switch (action.type) { - case actions.ADD_RECIPES: - return Object.assign({}, state, { - recipes: action.recipes, - }); - default: - return state; - } -}; - -export default recipe; diff --git a/src/client/sagas/add.js b/src/client/sagas/add.js deleted file mode 100644 index 2be08e1..0000000 --- a/src/client/sagas/add.js +++ /dev/null @@ -1,16 +0,0 @@ -import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'; -import * as actions from '../constants'; -import { action } from '../helpers/actionCreator'; - -function* addnumber(action) { - try { - yield put({ type: actions.SAVE, number: '1' }); - } catch (e) { - yield put({ type: 'ADD_FAILED', message: e.message }); - } -} - -function* addWatcher() { - yield takeEvery(actions.ADD, addnumber); -} -export default addWatcher; diff --git a/src/client/sagas/calc.js b/src/client/sagas/calc.js new file mode 100644 index 0000000..b51ad1f --- /dev/null +++ b/src/client/sagas/calc.js @@ -0,0 +1,56 @@ +import { put, takeEvery } from 'redux-saga/effects'; +import * as actions from '../constants'; + +function* initalize() { + try { + yield put({ + type: actions.SAVE, + number: 100, + }); + } catch (e) { + yield put({ type: 'INITALIZE_APPLICATION_FAILED', number: null }); + } +} + +export function* initalizeWatcher() { + yield takeEvery(actions.INITALIZE_APPLICATION, initalize); +} + +function* addnumber(action) { + try { + yield put({ type: actions.SAVE, number: action.payload.number + 1 }); + } catch (e) { + yield put({ type: 'ADD_FAILED', message: e.message }); + } +} + +export function* addWatcher() { + yield takeEvery(actions.ADD, addnumber); +} +export default addWatcher; + + +function* changenumber(action) { + try { + yield put({ type: actions.SAVE, number: action.payload.number }); + } catch (e) { + yield put({ type: 'CHANGE_FAILED', message: e.message }); + } +} + +export function* changeWatcher() { + yield takeEvery(actions.CHANGE, changenumber); +} + + +function* subnumber(action) { + try { + yield put({ type: actions.SAVE, number: action.payload.number - 1 }); + } catch (e) { + yield put({ type: 'SUB_FAILED', message: e.message }); + } +} + +export function* subWatcher() { + yield takeEvery(actions.SUB, subnumber); +} \ No newline at end of file diff --git a/src/client/sagas/combined.js b/src/client/sagas/combined.js index 1e7a30d..de8e82f 100644 --- a/src/client/sagas/combined.js +++ b/src/client/sagas/combined.js @@ -1,6 +1,5 @@ import { all } from 'redux-saga/effects'; -import addWatcher from './add'; -import startWatcher from './start'; +import * as watcher from './calc'; export default function* rootSaga() { - yield all([addWatcher(), startWatcher()]); + yield all([watcher.addWatcher(), watcher.subWatcher(), watcher.changeWatcher(), watcher.initalizeWatcher()]); } diff --git a/src/client/sagas/start.js b/src/client/sagas/start.js deleted file mode 100644 index 279c396..0000000 --- a/src/client/sagas/start.js +++ /dev/null @@ -1,19 +0,0 @@ -import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'; -import * as actions from '../constants'; -import { callFetch } from '../services/api'; -function* initalize() { - try { - const recipes = yield call(callFetch, '/recipes'); - yield put({ - type: actions.ADD_RECIPES, - recipes: JSON.parse(recipes.response), - }); - } catch (e) { - yield put({ type: 'START_APPLICATION_FAILED', recipes: [] }); - } -} - -function* startWatcher() { - yield takeEvery(actions.START_APPLICATION, initalize); -} -export default startWatcher; diff --git a/src/client/styles/App.scss b/src/client/styles/App.scss index 1247c62..df15a71 100644 --- a/src/client/styles/App.scss +++ b/src/client/styles/App.scss @@ -4,4 +4,13 @@ background-color: #fff; text-transform: capitalize; text-align: left; + .btn { + margin: 10px; + } + .form-control { + margin: 10px; + display: unset; + width: unset; + padding-bottom: 9px; + } } diff --git a/src/dist/index.html b/src/dist/index.html index dc89d6b..6fcb732 100644 --- a/src/dist/index.html +++ b/src/dist/index.html @@ -10,6 +10,7 @@
+ diff --git a/test/mocha.opts b/test/mocha.opts index 213b8e7..33bc6bc 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -4,5 +4,5 @@ --require @babel/register --require babel-polyfill --require test/mochasetup.js ---require jsdom +--require jsdom-global/register test/unit/**/*.{js,jsx} \ No newline at end of file diff --git a/test/unit/client/actions/indexTest.js b/test/unit/client/actions/indexTest.js new file mode 100644 index 0000000..c8cbd92 --- /dev/null +++ b/test/unit/client/actions/indexTest.js @@ -0,0 +1,28 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import * as action from '../../../../src/client/actions'; +import * as actions from '../../../../src/client/constants'; + +describe('actions', () => { + it('should trigger the ADD action', () => { + const number = 100; + const response = action.add(number); + + expect(response.type).to.deep.equal(actions.ADD); + expect(response.payload.number).to.deep.equal(100); + }); + it('should trigger the SUB action', () => { + const number = 100; + const response = action.sub(number); + + expect(response.type).to.deep.equal(actions.SUB); + expect(response.payload.number).to.deep.equal(100); + }); + it('should trigger the CHANGE action', () => { + const number = 100; + const response = action.change(number); + + expect(response.type).to.deep.equal(actions.CHANGE); + expect(response.payload.number).to.deep.equal(100); + }); +}); diff --git a/test/unit/client/components/AppTest.js b/test/unit/client/components/AppTest.js index dda0373..9613bf4 100644 --- a/test/unit/client/components/AppTest.js +++ b/test/unit/client/components/AppTest.js @@ -1,17 +1,39 @@ +import { describe, it, beforeEach } from 'mocha'; import React from 'react'; -import expect from 'expect'; -import Enzyme from 'enzyme'; +import Enzyme, { mount, shallow } from 'enzyme'; +import td from 'testdouble'; import Adapter from 'enzyme-adapter-react-16'; Enzyme.configure({ adapter: new Adapter() }); -import { shallow, mount } from 'enzyme'; +import chai, { expect } from 'chai'; +import chaiEnzyme from 'chai-enzyme'; +chai.use(chaiEnzyme()); import App from '../../../../src/client/components/App'; -let wrapper = shallow(); -describe('App Component', () => { + +describe.only('App Component', () => { + let wrapper, addspy, subspy, changespy; + beforeEach(function () { + addspy = td.function(); + subspy = td.function(); + changespy = td.function(); + wrapper = mount(); + }); it('renders div', () => { - expect(wrapper.find('div').text()).toContain('Welcome'); + expect(wrapper.find('div').exists()).to.be.true; + expect(wrapper.props().number).to.equal(100); + }); + it('should render two button', () => { + expect(wrapper.find('button').hostNodes()).to.have.lengthOf(2); + }); + it('verify add click', () => { + wrapper.find('button').hostNodes().at(0).simulate('click'); + td.verify(addspy(100)); + }); + it('verify sub click', () => { + wrapper.find('button').hostNodes().at(1).simulate('click'); + td.verify(subspy(100)); }); - it('renders react player div', () => { - wrapper = shallow(); - expect(wrapper.find('div').text()).toContain(''); + it('should call text box value change event', () => { + wrapper.instance().handleChange({ 'target': { 'value': 300 } }); + td.verify(changespy(300)); }); }); diff --git a/test/unit/client/containers/AppTest.js b/test/unit/client/containers/AppTest.js new file mode 100644 index 0000000..e69de29