Skip to content

Commit

Permalink
Merge pull request #20 from copios-jp/master
Browse files Browse the repository at this point in the history
Zone Timer
  • Loading branch information
copios authored Dec 13, 2018
2 parents eb56bed + 034b912 commit 112bf5a
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 19 deletions.
44 changes: 26 additions & 18 deletions app/renderer/components/sensor/edit/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ import { withStyles } from '@material-ui/core/styles'
import styles from '../../../styles/'

class Form extends Component {
fields = ({ age, weight, name, method, sex, max }) => {
return (
<Grid item xs>
{this.textField('name', '様', '名前', name)}
{this.textField('sex', '性', '性別', sex, {
select: true,
children: this.optionsFor(sexes),
})}
{this.textField('age', '才', '年齢', age)}
{this.textField('weight', 'Kg', '体重', weight)}
{this.textField('method', '', '最大心拍数計算式', method, {
select: true,
children: this.optionsFor(methods),
})}
{this.textField('max', 'BPM', '最大心拍数', max, {
disabled: method !== MANUAL,
type: 'number',
})}
</Grid>
)
}

handleFieldChange = (name) => (event) => {
const data = { ...this.props.data }
const value = event.target.value
Expand All @@ -32,27 +54,13 @@ class Form extends Component {
}

render() {
const { age, weight, name, method, coefficients, sex } = this.props.data
const { coefficients } = this.props.data
const max = getMaxHeartRate(this.props.data)
const disableMax = method !== MANUAL
return (
<Grid container spacing={24}>
{this.fields({ ...this.props.data, max: getMaxHeartRate(this.props.data) })}
<Grid item xs>
{this.textField('name', '様', '名前', name)}
{this.textField('sex', '性', '性別', sex, {
select: true,
children: this.optionsFor(sexes),
})}
{this.textField('age', '才', '年齢', age)}
{this.textField('weight', 'Kg', '体重', weight)}
{this.textField('method', '', '最大心拍数計算式', method, {
select: true,
children: this.optionsFor(methods),
})}
{this.textField('max', 'BPM', '最大心拍数', max, { disabled: disableMax })}
</Grid>
<Grid item xs>
<Coefficients coefficients={coefficients} max={max} />
<Coefficients coefficients={coefficients} max={max || 1} />
</Grid>
</Grid>
)
Expand All @@ -68,7 +76,7 @@ class Form extends Component {
className={classes.editTextField}
variant="outlined"
label={label}
value={value}
value={value || ''}
InputProps={{
endAdornment: <InputAdornment position="start">{adornment}</InputAdornment>,
}}
Expand Down
2 changes: 2 additions & 0 deletions app/renderer/components/sensor/header/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import React from 'react'
import ActivityIndicator from '../activity_indicator'
import { withStyles } from '@material-ui/core/styles'
import styles from '../../../styles/'
import StopWatch from '../../stop_watch/'

export const Header = (props) => {
const { classes, sensor } = props
return (
<div className={classes.cardHeader}>
<ActivityIndicator fontSize="small" active={sensor.active} />
<div className={classes.cardName}>{sensor.name}&nbsp;</div>
<StopWatch sensor={sensor} />
</div>
)
}
Expand Down
53 changes: 53 additions & 0 deletions app/renderer/components/stop_watch/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react'
import { Component } from 'react'
import Timer from '../../../services/timer/'
import { withStyles } from '@material-ui/core/styles'
import styles from '../../styles/'

import { getZone } from '../../../services/analytics/'

const pad = (integer) => {
return integer < 10 ? `0${integer}` : integer.toString()
}

class StopWatch extends Component {
componentDidMount() {
this.timer.on('tick', this.update)
this.timer.start()
}

componentDidUpdate = (prevProps) => {
if (getZone(prevProps.sensor) != getZone(this.props.sensor)) {
this.timer.reset()
}
}

componentWillUnmount() {
this.timer.stop()
}

getFormattedTime() {
const { time } = this.state
const minutes = Math.round(time / 60)
const seconds = Math.round(time % 60)
return `${pad(minutes)}:${pad(seconds)}`
}

render = () => {
const { classes } = this.props
const time = this.getFormattedTime()
return <div className={classes.timer}>{time}</div>
}

state = {
time: this.props.time || 0,
}

timer = new Timer()

update = (time) => {
this.setState({ ...this.state, time })
}
}

export default withStyles(styles)(StopWatch)
99 changes: 99 additions & 0 deletions app/renderer/components/stop_watch/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react'
import { shallow } from 'enzyme'
import { MANUAL } from '../../../services/analytics/MaxHeartRateCalculators/'
import { DEFAULT_ZONE_COEFFICIENTS } from '../sensor/'

jest.mock('../../../services/timer/', () => {
return function() {
this.on = jest.fn()
this.start = jest.fn()
this.stop = jest.fn()
this.reset = jest.fn()
this.value = 61
}
})

import StopWatch from './'

describe('Timer', () => {
let subject
let instance

const defaultProps = {
time: 61,
sensor: {
method: MANUAL,
rate: 50,
max: 100,
coefficients: [].concat(DEFAULT_ZONE_COEFFICIENTS),
},
}

const setup = () => {
subject = shallow(<StopWatch {...defaultProps} />)
instance = subject.dive().instance()
}

beforeAll(() => {
setup()
})

describe('component did mount', () => {
it('registers timer.on tick', () => {
const instance = subject.dive().instance()
expect(instance.timer.on).toBeCalledWith('tick', instance.update)
})
it('starts the timer', () => {
expect(instance.timer.start).toBeCalled()
})
})

describe('component will unmount', () => {
beforeEach(() => {
instance.componentWillUnmount()
})

it('stops the timer', () => {
expect(instance.timer.stop).toBeCalled()
})
})

describe('component did update', () => {
describe('same zone', () => {
beforeEach(() => {
instance.componentDidUpdate(defaultProps)
})
it('does not reset the timer', () => {
expect(instance.timer.reset).not.toBeCalled()
})
})

describe('different zone', () => {
beforeEach(() => {
const nextProps = {
sensor: { ...defaultProps.sensor, rate: 70 },
}
instance.props = nextProps
instance.componentDidUpdate(defaultProps)
})
it('resets the timer', () => {
expect(instance.timer.reset).toBeCalled()
})
})
})

describe('render', () => {
it('renders a div with formatted time', () => {
expect(subject.dive().text()).toMatch(/01:01/)
})
})

describe('update', () => {
beforeEach(() => {
instance.update(71)
})
it('updates state.time', () => {
expect(instance.state.time).toEqual(71)
})
})
})
4 changes: 4 additions & 0 deletions app/renderer/styles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export default (theme) => ({
alignItems: 'center',
},

timer: {
paddingRight: theme.spacing.unit,
},

activityIndicator: {
marginLeft: theme.spacing.unit,
},
Expand Down
4 changes: 3 additions & 1 deletion app/services/analytics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { FOX as fox } from './MaxHeartRateCalculators'
export const getMaxHeartRate = (sensor) => {
const { method } = sensor
const calculator = MaxHeartRateCalculators.forMethod(method)
return Math.round(calculator.using(sensor))
const rate = calculator.using(sensor)

return rate ? Math.round(calculator.using(sensor)) : undefined
}

export const getCalories = (sensor) => {
Expand Down
38 changes: 38 additions & 0 deletions app/services/timer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import events from 'events'
import NanoTimer from 'nanotimer'

const HOUR_MINUTES = 60
const MINUTE_SECONDS = 60
export const MAX_DURATION = HOUR_MINUTES * MINUTE_SECONDS

export default class Timer extends events.EventEmitter {
timer = new NanoTimer()

value = 0

reset = () => {
this.value = 0
this.emit('tick', this.value)
}

tick = () => {
this.value++

if (this.value > MAX_DURATION) {
this.stop()
} else {
this.emit('tick', this.value)
}
}

start = () => {
this.timer.setInterval(this.tick, '', '1s')
}

stop = () => {
this.timer.clearInterval()
this.value = 0
this.emit('tick', this.value)
this.removeAllListeners()
}
}
93 changes: 93 additions & 0 deletions app/services/timer/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Timer from './'
import lolex from 'lolex'
import { MAX_DURATION } from './'

describe('timer', () => {
let timer
let clock
const setup = () => {
clock = lolex.install()
timer = new Timer()

jest.spyOn(timer, 'removeAllListeners')
jest.spyOn(timer, 'emit')
jest.spyOn(timer.timer, 'setInterval')
jest.spyOn(timer.timer, 'clearInterval').mockImplementation(jest.fn())
timer.start()
clock.tick(1000)
}

const teardown = () => {
clock = clock.uninstall()
timer.removeAllListeners.mockRestore()
timer.emit.mockRestore()
timer.stop()
}

describe('start', () => {
beforeEach(() => {
setup()
})
afterEach(teardown)
it('has an interval', () => {
expect(timer.timer.setInterval).toBeCalledWith(timer.tick, '', '1s')
})
})

describe('reset', () => {
beforeEach(setup)
afterEach(teardown)
it('value is 0', () => {
expect(timer.value).toEqual(1)
timer.reset()
expect(timer.value).toEqual(0)
})
})

describe('stop', () => {
beforeEach(() => {
setup()
jest.spyOn(timer.timer, 'clearInterval')
timer.stop()
})
afterEach(teardown)

it('clears the interval', () => {
expect(timer.timer.clearInterval).toBeCalled()
})

it('value is 0', () => {
expect(timer.value).toEqual(0)
})

it('remmoves listeners', () => {
expect(timer.removeAllListeners).toBeCalled()
})

it('emits value of 0', () => {
expect(timer.emit).toBeCalledWith('tick', 0)
})
})

describe('tick', () => {
describe('valid', () => {
beforeEach(setup)
afterEach(teardown)
it('emits tick', () => {
expect(timer.emit).toBeCalledWith('tick', 1)
})
})

describe('running for more than an hour', () => {
beforeAll(() => {
setup()
jest.spyOn(timer, 'stop')
clock.tick(MAX_DURATION * 1000)
})

it('calls stop when the timer has run longer than allowed', () => {
expect(timer.stop).toBeCalled()
})
})
})
})
Loading

0 comments on commit 112bf5a

Please sign in to comment.