Skip to content

Commit

Permalink
feat(ProMetronome): Add 'isPlaying' prop, 'bpm' and 'subdivision' cus…
Browse files Browse the repository at this point in the history
…tom prop-type and corresponding

Add 'isPlaying' prop to allow to play/stop the metronome. Add 'bpm' and 'subdivision' custom
prop-type to check number type and range. Add corresponding tests to verify all this new features.
  • Loading branch information
rigobauer committed Feb 9, 2018
1 parent 2a31140 commit feca330
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 18 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -63,6 +63,7 @@ This will render (at 95 bpm)....
| :----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------: | :--------------------------------------: | :-----: |
| bpm | Set the click speed (_beeps per minute_). | number | 1-300 | 80 |
| subdivision | Set the number of notes(clicks) you want to have from one quarter note and the next one. For example, if you want your metronome to have a click in 16th notes, you'll have to set subdivision at 4 (each quarter note has four 16th note). | number | 1-8 | 1 |
| isPlaying | Play/Stop the metronome | boolean | true/false | true |
| soundEnabled | Enable/disable all click sounds. | boolean | true/false | false |
| soundPattern | Define the sound level for each one of the notes of a bar, including subdivisions. It's a string that has to have the length of the number of notes you have in a bar (including subdivisions) in which each character define the sound level: '0' (no sound), '1' (low sound), '2' (medium sound) and '3' (high sound). | string | String composed by: '0', '1', '2' or '3' | '' |
| render | Function where you can define what you want the component to render. | function | function | - |
Expand Down
11 changes: 10 additions & 1 deletion demo/src/index.js
Expand Up @@ -6,7 +6,8 @@ import ProMetronome from '../../src'
class Demo extends Component {
state = {
bpm1: 90,
bpm2: 70
bpm2: 70,
isPlaying2: false
}

componentDidMount() {
Expand Down Expand Up @@ -48,6 +49,13 @@ class Demo extends Component {
<div>subdivision: {props.subdivision}</div>
<div>quarter note: {state.qNote}</div>
<div>subdivision note: {state.subNote}</div>
<button
onClick={() => {
this.setState(prevState => ({ isPlaying2: !prevState.isPlaying2 }))
}}
>
{props.isPlaying ? 'STOP' : 'PLAY'}
</button>
</div>
)

Expand All @@ -64,6 +72,7 @@ class Demo extends Component {
<ProMetronome
bpm={this.state.bpm2}
subdivision={5}
isPlaying={this.state.isPlaying2}
soundEnabled={true}
soundPattern="30011200112001120011"
render={this.metronomeStyle2}
Expand Down
98 changes: 87 additions & 11 deletions src/index.js
Expand Up @@ -14,6 +14,9 @@ import click1SoundFileMP3 from './sounds/click1.mp3'
import click1SoundFileOGG from './sounds/click1.ogg'
import click1SoundFileAAC from './sounds/click1.aac'

const MAXBPM = 300
const MAXSUBDIVISION = 8

class ProMetronome extends PureComponent {
state = {
qNote: 1,
Expand Down Expand Up @@ -64,18 +67,35 @@ class ProMetronome extends PureComponent {
}

componentDidMount() {
this.timerID = setInterval(
this.update,
this.calculateInterval(this.props.bpm, this.props.subdivision)
)
if (this.props.isPlaying) {
this.timerID = setInterval(
this.update,
this.calculateInterval(this.props.bpm, this.props.subdivision)
)
}
}

componentWillReceiveProps(nextProps) {
clearInterval(this.timerID)
this.timerID = setInterval(
this.update,
this.calculateInterval(nextProps.bpm, nextProps.subdivision)
)
if (nextProps.isPlaying != this.props.isPlaying) {
if (nextProps.isPlaying) {
this.timerID = setInterval(
this.update,
this.calculateInterval(nextProps.bpm, nextProps.subdivision)
)
} else {
clearInterval(this.timerID)
}
} else if (
nextProps.isPlaying &&
(nextProps.bpm != this.props.bpm ||
nextProps.subdivision != this.props.subdivision)
) {
clearInterval(this.timerID)
this.timerID = setInterval(
this.update,
this.calculateInterval(nextProps.bpm, nextProps.subdivision)
)
}
}

componentWillUnmount() {
Expand All @@ -88,8 +108,63 @@ class ProMetronome extends PureComponent {
}

ProMetronome.propTypes = {
bpm: PropTypes.number,
subdivision: PropTypes.number,
bpm: function(props, propName, componentName) {
if (props[propName]) {
const propValue = props[propName],
propType = typeof propValue
if (propType !== 'number')
return new Error(
'Invalid prop `' +
propName +
'` of type `' +
propType +
'` supplied to ' +
componentName +
', expected `number`.'
)
if (propValue < 1 || propValue > MAXBPM)
return new Error(
'Invalid prop `' +
propName +
'` with value ' +
propValue +
' supplied to ' +
componentName +
'. Allowed range is 1-' +
MAXBPM +
'.'
)
}
},
subdivision: function(props, propName, componentName) {
if (props[propName]) {
const propValue = props[propName],
propType = typeof propValue
if (propType !== 'number')
return new Error(
'Invalid prop `' +
propName +
'` of type `' +
propType +
'` supplied to ' +
componentName +
', expected `number`.'
)
if (propValue < 1 || propValue > MAXSUBDIVISION)
return new Error(
'Invalid prop `' +
propName +
'` with value ' +
propValue +
' supplied to ' +
componentName +
'. Allowed range is 1-' +
MAXSUBDIVISION +
'.'
)
}
},
isPlaying: PropTypes.bool,
soundEnabled: PropTypes.bool,
soundPattern: function(props, propName, componentName) {
if (props[propName]) {
Expand Down Expand Up @@ -125,6 +200,7 @@ ProMetronome.propTypes = {
ProMetronome.defaultProps = {
bpm: 80,
subdivision: 1,
isPlaying: true,
soundEnabled: false,
soundPattern: ''
}
Expand Down
52 changes: 46 additions & 6 deletions tests/index.test.js
Expand Up @@ -46,6 +46,7 @@ describe('<ProMetronome />', () => {
<ProMetronome
bpm={80}
subdivision={4}
isPlaying={false}
soundEnabled={true}
soundPattern="3222322232223222"
render={(props, state) => (
Expand All @@ -57,10 +58,17 @@ describe('<ProMetronome />', () => {
)

expect(wrapper.text()).to.equal('1/1')
clock.tick(5 * interval + 5)
expect(wrapper.text()).to.equal('1/1')
wrapper.setProps({ isPlaying: true })
clock.tick(interval + 5)
expect(wrapper.text()).to.equal('1/2')
clock.tick(interval + 5)
expect(wrapper.text()).to.equal('1/3')
wrapper.setProps({ isPlaying: false })
clock.tick(interval + 5)
expect(wrapper.text()).to.equal('1/3')
wrapper.setProps({ isPlaying: true })
clock.tick(interval + 5)
expect(wrapper.text()).to.equal('1/4')
clock.tick(interval + 5)
Expand All @@ -75,14 +83,14 @@ describe('<ProMetronome />', () => {
sinon.assert.callCount(Howl.prototype.play, 16)

wrapper.setProps({ bpm: 100, subdivision: 2, soundPattern: '32323232' })
expect(
ProMetronome.prototype.componentWillReceiveProps.calledOnce
).to.equal(true)
expect(ProMetronome.prototype.componentWillReceiveProps.callCount).to.equal(
4
)
interval = Math.floor(60000 / (100 * 2))
clock.tick(interval + 5)
expect(wrapper.text()).to.equal('1/2')

expect(ProMetronome.prototype.render.callCount).to.equal(19)
expect(ProMetronome.prototype.render.callCount).to.equal(22)
expect(ProMetronome.prototype.componentWillUnmount.notCalled).to.equal(true)
wrapper.unmount()
expect(ProMetronome.prototype.componentWillUnmount.calledOnce).to.equal(
Expand All @@ -99,9 +107,9 @@ describe('<ProMetronome />', () => {
let interval = Math.floor(60000 / (80 * 4))
const wrapper = mount(
<ProMetronome
bpm={80}
bpm="80"
subdivision={2}
soundPattern={32323232}
soundEnabled={false}
render={(props, state) => (
<div>
{state.qNote}/{state.subNote}
Expand All @@ -110,6 +118,38 @@ describe('<ProMetronome />', () => {
/>
)

sinon.assert.callCount(console.error, 1)
sinon.assert.calledWithMatch(
console.error,
'Warning: Failed prop type: Invalid prop `bpm` of type `string` supplied to ProMetronome, expected `number`.'
)
console.error.resetHistory()
wrapper.setProps({ bpm: 350 })
sinon.assert.callCount(console.error, 1)
sinon.assert.calledWithMatch(
console.error,
'Warning: Failed prop type: Invalid prop `bpm` with value 350 supplied to ProMetronome. Allowed range is 1-300.'
)
console.error.resetHistory()
wrapper.setProps({ bpm: 80, subdivision: '2' })
sinon.assert.callCount(console.error, 1)
sinon.assert.calledWithMatch(
console.error,
'Warning: Failed prop type: Invalid prop `subdivision` of type `string` supplied to ProMetronome, expected `number`.'
)
console.error.resetHistory()
wrapper.setProps({ subdivision: 12 })
sinon.assert.callCount(console.error, 1)
sinon.assert.calledWithMatch(
console.error,
'Warning: Failed prop type: Invalid prop `subdivision` with value 12 supplied to ProMetronome. Allowed range is 1-8.'
)
console.error.resetHistory()
wrapper.setProps({
subdivision: 2,
soundEnabled: true,
soundPattern: 323232323
})
sinon.assert.callCount(console.error, 1)
sinon.assert.calledWithMatch(
console.error,
Expand Down

0 comments on commit feca330

Please sign in to comment.