diff --git a/package.json b/package.json index 7b64e09..1b0c62d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,13 @@ ] } }, + "nyc": { + "exclude": [ + "**/*.spec.js", + "**/*.mock.js", + "build" + ] + }, "repository": { "type": "git", "url": "git+https://github.com/khdkhd/wasa" @@ -84,4 +91,4 @@ "rxjs": "^5.4.3", "worker-timer": "^1.1.0" } -} +} \ No newline at end of file diff --git a/src/blocks/hat.spec.js b/src/blocks/hat.spec.js new file mode 100644 index 0000000..6acc162 --- /dev/null +++ b/src/blocks/hat.spec.js @@ -0,0 +1,56 @@ +import test from 'ava' +import sinon from 'sinon' +import { Hat } from './hat' +import { AudioContextMock } from '../mock/audio-context.mock' + +test('Hat factory returns object', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const hat = Hat(audioContext) + t.true(typeof hat === 'object') +}) + +test('Hat factory returns object with a duration getter and setter', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const hat = Hat(audioContext) + hat.setDuration(1) + t.is(1, hat.getDuration()) +}) + +test('Hat connect method returns an object with a connect method', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const hat = Hat(audioContext) + const nextInChain = { + input: audioContext.createGain(), + connect() {}, + } + t.true(typeof hat.connect(nextInChain).connect === 'function') +}) + +test('Hat noteOn method call create oscillators in the audio context', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const hat = Hat(audioContext) + hat.noteOn() + t.true(audioContext.createOscillator.called) +}) + +test('Hat noteOff method call stop on oscillator nodes', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const hat = Hat(audioContext) + hat.noteOn() + hat.noteOff() + audioContext.getOscillatorNodes() + .forEach((osc) => { + t.true(osc.stop.called) + }) +}) + +test('Hat noteOff method cancel scheduled values on gain nodes', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const hat = Hat(audioContext) + hat.noteOn() + hat.noteOff() + audioContext.getGainNodes() + .forEach((gain) => { + t.true(gain.gain.cancelScheduledValues.called) + }) +}) diff --git a/src/blocks/kick.spec.js b/src/blocks/kick.spec.js new file mode 100644 index 0000000..11380dd --- /dev/null +++ b/src/blocks/kick.spec.js @@ -0,0 +1,72 @@ +import test from 'ava' +import sinon from 'sinon' +import { Kick } from './kick' +import { AudioContextMock } from '../mock/audio-context.mock' + +test('Kick factory returns object', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const kick = Kick(audioContext) + t.true(typeof kick === 'object') +}) + +test('Kick factory returns object with a duration getter and setter', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const kick = Kick(audioContext) + kick.setDuration(1) + t.is(1, kick.getDuration()) +}) + +test('Kick factory returns object with a frequency getter and setter', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const kick = Kick(audioContext) + kick.setFrequency(440) + t.is(440, kick.getFrequency()) +}) + +test('Kick factory returns object with a finalFrequency getter and setter', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const kick = Kick(audioContext) + kick.setFinalFrequency(220) + t.is(220, kick.getFinalFrequency()) +}) + +test('Kick connect method returns an object with a connect method', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const kick = Kick(audioContext) + const nextInChain = { + input: audioContext.createGain(), + connect() {}, + } + t.true(typeof kick.connect(nextInChain).connect === 'function') +}) + +test('Kick noteOn method call create oscillators in the audio context', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const kick = Kick(audioContext) + kick.noteOn() + t.true(audioContext.createOscillator.called) +}) + +test('Kick noteOff method call stop on oscillator nodes', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const kick = Kick(audioContext) + kick.noteOn() + kick.noteOff() + audioContext.getOscillatorNodes() + .forEach((osc) => { + t.true(osc.stop.called) + }) +}) + +test('Kick noteOff method cancel scheduled values on osc gain nodes', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const kick = Kick(audioContext) + kick.noteOn() + kick.noteOff() + let nG = 0 // number of gain nodes + audioContext.getGainNodes() + .forEach((gain) => { + nG += gain.gain.cancelScheduledValues.called ? 1 : 0 + }) + t.is(audioContext.getGainNodes().length - 1, nG) +}) diff --git a/src/common/range.spec.js b/src/common/range.spec.js index 1db5978..21c8a55 100644 --- a/src/common/range.spec.js +++ b/src/common/range.spec.js @@ -10,6 +10,12 @@ test('Normalizes value from [min,max] to [0,1]', (t) => { t.is(0.5, value) }) +test('Returns value unchanged if range is undefined', (t) => { + const range = undefined + const value = scale(range, 10) + t.is(value, value) +}) + test('Unormalizes value from [0,1] to [min,max]', (t) => { const range = { min: 5, @@ -18,3 +24,10 @@ test('Unormalizes value from [0,1] to [min,max]', (t) => { const value = unscale(range, 0.5) t.is(10, value) }) + + +test('Returns value unchanged if range is undefined', (t) => { + const range = undefined + const value = unscale(range, 10) + t.is(value, value) +}) diff --git a/src/core/note.spec.js b/src/core/note.spec.js index 9e847bd..ecb433d 100644 --- a/src/core/note.spec.js +++ b/src/core/note.spec.js @@ -1,6 +1,41 @@ import test from 'ava' -import { getFrequency } from '.' +import { Note, getFrequency, DURATIONS } from '.' +test('Note factory creates an object', (t) => { + const note = Note({ + note: 'A4', + octave: 3, + duration: DURATIONS.WHOLE, + }) + t.true(typeof note === 'object') +}) + +test('Note factory creates an object with a note getter', (t) => { + const note = Note({ + note: 'A4', + octave: 3, + duration: DURATIONS.WHOLE, + }) + t.is('A4', note.getNote()) +}) + +test('Note factory creates an object with an octave getter', (t) => { + const note = Note({ + note: 'A4', + octave: 3, + duration: DURATIONS.WHOLE, + }) + t.is(3, note.getOctave()) +}) + +test('Note factory creates an object with a duration getter', (t) => { + const note = Note({ + note: 'A4', + octave: 3, + duration: DURATIONS.WHOLE, + }) + t.is(DURATIONS.WHOLE, note.getDuration()) +}) test('Note C3 converts to 130.81 Hz', (t) => { const frequency = getFrequency('C', 3) t.is(130.81, Number(frequency.toFixed(2))) diff --git a/src/core/sequencer.spec.js b/src/core/sequencer.spec.js index ced5252..7e39309 100644 --- a/src/core/sequencer.spec.js +++ b/src/core/sequencer.spec.js @@ -3,6 +3,12 @@ import sinon from 'sinon' import { Sequencer } from '.' import { AudioContextMock } from '../mock/audio-context.mock' +test('Sequencer factory creates a sequencer object', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const sequencer = Sequencer(audioContext) + t.true(typeof sequencer === 'object') +}) + test('Calling start triggers onPlay handler', (t) => { const audioContext = AudioContextMock(sinon.sandbox.create()) Sequencer(audioContext) @@ -11,3 +17,41 @@ test('Calling start triggers onPlay handler', (t) => { }) .start() }) + +test('Calling stop triggers onStop handler', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + Sequencer(audioContext) + .onStop(() => { + t.pass() + }) + .start() + .stop() +}) + +test('Sequencer factory returns object with a loopMode getter and setter', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const sequencer = Sequencer(audioContext) + sequencer.setLoopMode(true) + t.is(true, sequencer.getLoopMode()) +}) + +test('Sequencer factory returns object with a length getter and setter', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const sequencer = Sequencer(audioContext) + sequencer.setLength(16) + t.is(16, sequencer.getLength()) +}) + +test('Sequencer factory returns object with a division getter and setter', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const sequencer = Sequencer(audioContext) + sequencer.setDivision(4) + t.is(4, sequencer.getDivision()) +}) + +test('Sequencer factory returns object with a tempo getter and setter', (t) => { + const audioContext = AudioContextMock(sinon.sandbox.create()) + const sequencer = Sequencer(audioContext) + sequencer.setTempo(120) + t.is(120, sequencer.getTempo()) +}) diff --git a/src/mock/audio-context.mock.js b/src/mock/audio-context.mock.js index 4048930..c81e319 100644 --- a/src/mock/audio-context.mock.js +++ b/src/mock/audio-context.mock.js @@ -14,6 +14,7 @@ export const AudioContextMock = (sandbox) => { setValueAtTime: sandbox.spy(), cancelScheduledValues: sandbox.spy(), linearRampToValueAtTime: sandbox.spy(), + exponentialRampToValueAtTime: sandbox.spy(), value: undefined, }) @@ -26,6 +27,7 @@ export const AudioContextMock = (sandbox) => { frequency: AudioParam(), connect: sandbox.spy(), start: sandbox.spy(), + stop: sandbox.spy(), }) const createChannelMerger = () => ({