Skip to content

Commit

Permalink
Release 1 (#2)
Browse files Browse the repository at this point in the history
* Added Timer

* Timer page

* Change folder for github pages
  • Loading branch information
dmitrij-borchuk authored Mar 12, 2018
1 parent ba05f3a commit fb4d7d1
Show file tree
Hide file tree
Showing 45 changed files with 1,125 additions and 71 deletions.
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"presets": [
"babel-preset-env",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
}
4 changes: 3 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"globals": {
"describe": true,
"it": true,
"expect": true
"expect": true,
"jest": true,
"afterEach": true
},
"rules": {
"function-paren-newline": [
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/node_modules
/yarn-error.log
/coverage
6 changes: 6 additions & 0 deletions client/.storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<style>
body {
font-family: 'Roboto', sans-serif;
}
</style>
5 changes: 5 additions & 0 deletions client/.storybook/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const generalConfig = require('../../webpack.config');

module.exports = {
module: generalConfig.module,
};
47 changes: 47 additions & 0 deletions client/actions/timer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { SECONDS_IN_SESSION } from '../constants';

let timeout = null;

export const SET_TIMER = 'SET_TIMER';
export const setTimer = seconds => ({
type: SET_TIMER,
payload: seconds >= 0 ? seconds : 0,
});

export const STOP_TIMER = 'STOP_TIMER';
export const stopTimer = () => {
clearTimeout(timeout);
return {
type: STOP_TIMER,
};
};

export const resetTimer = () => (dispatch) => {
dispatch(stopTimer());
dispatch(setTimer(SECONDS_IN_SESSION));
clearTimeout(timeout);
};

export const START_TIMER = 'START_TIMER';
export const startTimer = () => (dispatch, getState) => {
const msInSec = 1000;
clearTimeout(timeout);
function updateTimer() {
const currentState = getState();
const newSeconds = currentState.timer.seconds - 1;
if (newSeconds <= 0) {
dispatch(stopTimer());
}
dispatch(setTimer(newSeconds));
timeout = setTimeout(updateTimer, msInSec);
}
const state = getState();
if (state.timer.seconds === 0) {
dispatch(resetTimer());
}
dispatch({
type: START_TIMER,
});

timeout = setTimeout(updateTimer, msInSec);
};
130 changes: 130 additions & 0 deletions client/actions/timer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { SECONDS_IN_SESSION } from '../constants';
import { createDispatch } from '../testHelpers';

import {
setTimer,
SET_TIMER,
resetTimer,
STOP_TIMER,
startTimer,
stopTimer,
} from './timer';

function getFirstCallForActionType(mock, type) {
return mock.mock.calls.filter(call => call[0].type === type)[0];
}

describe('Timer actions', () => {
it('setTimer ', () => {
const seconds = 50;
const action = setTimer(seconds);

expect(action).toEqual({ type: SET_TIMER, payload: seconds });
});
it('`setTimer` action should not count less than 0 minutes or seconds ', () => {
const seconds = -50;
const action = setTimer(seconds);

expect(action).toEqual({ type: SET_TIMER, payload: 0 });
});
it('`resetTimer` action should set seconds to max value', () => {
const dispatchMock = jest.fn();
resetTimer()(dispatchMock);
const actionCall = dispatchMock.mock.calls.filter(call => call[0].type === SET_TIMER)[0];

expect(actionCall[0]).toEqual({ type: SET_TIMER, payload: SECONDS_IN_SESSION });
});
it('`resetTimer` action should stop timer', () => {
const dispatchMock = jest.fn();
resetTimer()(dispatchMock);
const actionCall = dispatchMock.mock.calls.filter(call => call[0].type === STOP_TIMER)[0];

expect(actionCall[0]).toEqual({ type: STOP_TIMER });
});
it('`startTimer` action should set max time for timer if time is 00:00', () => {
const state = {
timer: {
seconds: 0,
},
};
const dispatchMock = jest.fn();
const dispatch = createDispatch(state, dispatchMock);

startTimer()(dispatch, () => state);

const actionCall = getFirstCallForActionType(dispatchMock, SET_TIMER);
expect(actionCall[0].payload).toEqual(SECONDS_IN_SESSION);
});
it('if timer is started it should count seconds from max to 0', () => {
jest.useFakeTimers();

const state = {
timer: {
seconds: SECONDS_IN_SESSION,
isStarted: false,
},
};
const dispatchMock = jest.fn();
const dispatch = createDispatch(state, dispatchMock);
const skipSeconds = 1;
const msInSec = 1000;

startTimer()(dispatch, () => state);
jest.advanceTimersByTime(msInSec * skipSeconds);

const actionCall = getFirstCallForActionType(dispatchMock, SET_TIMER);
expect(actionCall[0].payload).toEqual(SECONDS_IN_SESSION - skipSeconds);
});
it('`stopTimer` should clear timeout', () => {
jest.useFakeTimers();

const state = {
timer: {
seconds: SECONDS_IN_SESSION,
isStarted: false,
},
};
const dispatchMock = jest.fn();
const dispatch = createDispatch(state, dispatchMock);
const skipSeconds = 1;
const msInSec = 1000;

startTimer()(dispatch, () => state);
jest.advanceTimersByTime(msInSec * skipSeconds);

let actionCall = getFirstCallForActionType(dispatchMock, SET_TIMER);
expect(actionCall[0].payload).toEqual(SECONDS_IN_SESSION - skipSeconds);
const callsCount = dispatchMock.mock.calls.length;

const stopDispatchMock = jest.fn();
const stopDispatch = createDispatch(state, stopDispatchMock);

stopDispatch(stopTimer());
jest.advanceTimersByTime(msInSec * skipSeconds);

actionCall = getFirstCallForActionType(dispatchMock, SET_TIMER);
expect(dispatchMock.mock.calls.length).toEqual(callsCount);
});
it('after reaching 0 timer should stop', () => {
jest.useFakeTimers();

const state = {
timer: {
seconds: 1,
isStarted: false,
},
};
const dispatchMock = jest.fn();
const dispatch = createDispatch(state, dispatchMock);
const msInSec = 1000;

startTimer()(dispatch, () => state);
state.timer.seconds -= 1;
jest.advanceTimersByTime(msInSec * 1);

const actionCall = getFirstCallForActionType(dispatchMock, STOP_TIMER);
expect(actionCall).toBeDefined();
});

afterEach(() => jest.clearAllTimers());
});
25 changes: 25 additions & 0 deletions client/components/App/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import Timer from '../Timer/container';
import TimerControls from '../TimerControls/container';
import styles from './styles.css';

export default function App() {
return (
<div className={styles.app}>
<div className={styles.circle}>
<div>
<Timer />
<div className={styles.controls}>
<TimerControls />
</div>
</div>
</div>
</div>
);
}

App.propTypes = {
};

App.defaultProps = {
};
14 changes: 14 additions & 0 deletions client/components/App/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

import React from 'react';
import { shallow } from 'enzyme';
import Component from './index';

describe('App component', () => {
it('should render application', () => {
const rendered = shallow(
<Component />,
);

expect(rendered.length).toEqual(1);
});
});
30 changes: 30 additions & 0 deletions client/components/App/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
:global #root, :global body, :global html {
height: 100%;
}

.app {
align-items: center;
background: #f44336;
color: #fff;
display: flex;
font-family: 'Roboto', sans-serif;
height: 100%;
justify-content: center;
}

.circle {
align-items: center;
border: 3px solid #b71c1c;
border-radius: 50%;
display: flex;
box-sizing: border-box;
justify-content: center;
height: 90vmin;
width: 90vmin;
}

.controls {
align-items: center;
display: flex;
justify-content: center;
}
42 changes: 42 additions & 0 deletions client/components/Button/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import css from './styles.css';

export default function Button(props) {
const {
children,
onClick,
type,
} = props;
const className = cn(css.button, css[type.toLowerCase()]);

return (
<button
className={className}
onClick={onClick}
>
{children}
</button>
);
}
Button.TYPES = {
PRIMARY: 'PRIMARY',
};

Button.propTypes = {
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.string,
]),
onClick: PropTypes.func,
type: PropTypes.oneOf([
Button.TYPES.PRIMARY,
]),
};

Button.defaultProps = {
children: '',
onClick: () => {},
type: Button.TYPES.PRIMARY,
};
30 changes: 30 additions & 0 deletions client/components/Button/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

import React from 'react';
import { shallow } from 'enzyme';
import Component from './index';

describe('Button component', () => {
it('should render text', () => {
const text = 'ButtonText';

const rendered = shallow(
<Component>
{text}
</Component>,
);

expect(rendered.text()).toEqual(text);
});
it('should have `onClick` event', () => {
const cb = jest.fn();

const rendered = shallow(
<Component onClick={cb}>
text
</Component>,
);
rendered.simulate('click');

expect(cb.mock.calls.length).toEqual(1);
});
});
15 changes: 15 additions & 0 deletions client/components/Button/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.button {
border: none;
cursor: pointer;
padding: 5px 10px;
transition: background-color 0.2s ease;
}

.primary {
background: none;
color: #fff;
}

.primary:hover {
background: #b71c1c;
}
10 changes: 10 additions & 0 deletions client/components/Timer/container.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import component from './index';

const mapStateToProps = ({ timer }) => ({
seconds: timer.seconds,
});

export default connect(
mapStateToProps,
)(component);
Loading

0 comments on commit fb4d7d1

Please sign in to comment.