diff --git a/package.json b/package.json index bde8aad..a19c742 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,7 @@ "dependencies": { "audio-context": "1.0.1", "minilog": "^3.0.1", - "startaudiocontext": "1.2.1", - "tap": "^12.0.1" + "startaudiocontext": "1.2.1" }, "devDependencies": { "babel-core": "^6.24.1", @@ -37,6 +36,7 @@ "json": "^9.0.6", "travis-after-all": "^1.4.4", "webpack": "^4.8.0", - "webpack-cli": "^2.0.15" + "webpack-cli": "^2.0.15", + "tap": "^12.0.1" } } diff --git a/test/__mocks__/AudioContext.js b/test/__mocks__/AudioContext.js new file mode 100644 index 0000000..dfc2dc3 --- /dev/null +++ b/test/__mocks__/AudioContext.js @@ -0,0 +1,20 @@ +const AudioNodeMock = require('./AudioNode'); +const AudioParamMock = require('./AudioParam'); + +class AudioContextMock { + createGain () { + return new AudioNodeMock({ + gain: new AudioParamMock({ + default: 1, + min: -3, + max: 3 + }) + }); + } + + createChannelMerger () { + return new AudioNodeMock(); + } +} + +module.exports = AudioContextMock; diff --git a/test/__mocks__/AudioEngine.js b/test/__mocks__/AudioEngine.js new file mode 100644 index 0000000..0196ec9 --- /dev/null +++ b/test/__mocks__/AudioEngine.js @@ -0,0 +1,12 @@ +const AudioContextMock = require('./AudioContext'); +const AudioTargetMock = require('./AudioTarget'); + +class AudioEngineMock extends AudioTargetMock { + constructor () { + super(); + + this.audioContext = new AudioContextMock(); + } +} + +module.exports = AudioEngineMock; diff --git a/test/__mocks__/AudioNode.js b/test/__mocks__/AudioNode.js new file mode 100644 index 0000000..c0ac3cf --- /dev/null +++ b/test/__mocks__/AudioNode.js @@ -0,0 +1,36 @@ +class AudioNodeMock { + constructor (params) { + Object.assign(this, params); + + this.connected = null; + this.connectedFrom = []; + this._testResult = []; + } + + connect (node) { + this.connected = node; + node.connectedFrom.push(this); + } + + disconnect () { + if (this.connected !== null) { + this.connectedFrom = this.connectedFrom.filter(connection => connection !== this); + } + this.connected = null; + } + + _test (test, message, depth = 0) { + if (this.connected === null) { + this._testResult.push({test, message, depth}); + } else { + this.connected._test(test, message, depth + 1); + this._testResult.push(null); + } + } + + _result () { + return this._testResult.pop(); + } +} + +module.exports = AudioNodeMock; diff --git a/test/__mocks__/AudioParam.js b/test/__mocks__/AudioParam.js new file mode 100644 index 0000000..8ddc9a5 --- /dev/null +++ b/test/__mocks__/AudioParam.js @@ -0,0 +1,7 @@ +class AudioParamMock { + setTargetAtTime (value /* , start, stop */) { + this.value = value; + } +} + +module.exports = AudioParamMock; diff --git a/test/__mocks__/AudioTarget.js b/test/__mocks__/AudioTarget.js new file mode 100644 index 0000000..da2ff49 --- /dev/null +++ b/test/__mocks__/AudioTarget.js @@ -0,0 +1,21 @@ +const AudioNodeMock = require('./AudioNode'); + +class AudioTargetMock { + constructor () { + this.inputNode = new AudioNodeMock(); + } + + connect (target) { + this.inputNode.connect(target.getInputNode()); + } + + getInputNode () { + return this.inputNode; + } + + getSoundPlayers () { + return {}; + } +} + +module.exports = AudioTargetMock; diff --git a/test/effects/EffectShape.js b/test/effects/EffectShape.js new file mode 100644 index 0000000..809e7dd --- /dev/null +++ b/test/effects/EffectShape.js @@ -0,0 +1,115 @@ +const tap = require('tap'); + +const PanEffect = require('../../src/effects/PanEffect'); +const PitchEffect = require('../../src/effects/PitchEffect'); +const VolumeEffect = require('../../src/effects/VolumeEffect'); + +const AudioEngine = require('../__mocks__/AudioEngine'); +const AudioTarget = require('../__mocks__/AudioTarget'); + +const testEffect = (EffectClass, effectDepth) => { + tap.test(EffectClass.name, t1 => { + t1.plan(3); + + t1.test('methods', t2 => { + t2.plan(7); + + t2.ok( + 'DEFAULT_VALUE' in EffectClass.prototype, + 'has DEFAULT_VALUE' + ); + t2.type(EffectClass.prototype.initialize, 'function', 'has initialize'); + t2.type(EffectClass.prototype.set, 'function', 'has set'); + t2.type(EffectClass.prototype.update, 'function', 'has update'); + t2.type(EffectClass.prototype.clear, 'function', 'has clear'); + t2.type(EffectClass.prototype.connect, 'function', 'has connect'); + t2.type(EffectClass.prototype.dispose, 'function', 'has dispose'); + + t2.end(); + }); + + t1.test('connect', t2 => { + t2.plan(6); + + const engine = new AudioEngine(); + const target = new AudioTarget(); + let effect = new EffectClass(engine, target, null); + + target.inputNode._test('stall', 'message does not move'); + t2.equal(target.inputNode._result().depth, 0, 'message not sent'); + t2.ok(!engine.inputNode._result(), 'message not received'); + t2.ok(!engine.inputNode.connected, 'engine not connected'); + + effect.dispose(); + effect = new EffectClass(engine, target, null); + const effect2 = new EffectClass(engine, target, effect); + effect2.connect(engine); + effect.connect(effect2); + + target.inputNode._test('move', 'message does move'); + t2.ok(!target.inputNode._result(), 'message sent'); + t2.equal(engine.inputNode._result().depth, 1, 'message received'); + t2.ok(engine.inputNode.connectedFrom.length, 'engine connected'); + + t2.end(); + }); + + t1.test('lifecycle', t2 => { + t2.plan(18); + + const engine = new AudioEngine(); + const target = new AudioTarget(); + let effect = new EffectClass(engine, target, null); + let effect2 = new EffectClass(engine, target, effect); + + target.inputNode._test('stall', 'message does not move'); + t2.equal(target.inputNode._result().depth, 0, 'message not sent'); + t2.ok(!engine.inputNode._result(), 'message not received'); + t2.ok(!engine.inputNode.connected, 'engine not connected'); + + effect2.connect(engine); + effect.connect(effect2); + + target.inputNode._test('move', 'message does move'); + t2.ok(!target.inputNode._result(), 'message sent'); + t2.equal(engine.inputNode._result().depth, 1, 'message received'); + t2.ok(engine.inputNode.connectedFrom.length, 'engine connected'); + + effect.set(effect.DEFAULT_VALUE - 1); + + target.inputNode._test('move', 'message does move'); + t2.ok(!target.inputNode._result(), 'message sent'); + t2.equal(engine.inputNode._result().depth, 1 + effectDepth, 'message received'); + t2.ok(engine.inputNode.connectedFrom.length, 'engine connected'); + + effect2.set(effect2.DEFAULT_VALUE - 1); + + target.inputNode._test('move', 'message does move'); + t2.ok(!target.inputNode._result(), 'message sent'); + t2.equal(engine.inputNode._result().depth, 1 + (2 * effectDepth), 'message received'); + t2.ok(engine.inputNode.connectedFrom.length, 'engine connected'); + + effect.clear(); + + target.inputNode._test('move', 'message does move'); + t2.ok(!target.inputNode._result(), 'message sent'); + t2.equal(engine.inputNode._result().depth, 1 + effectDepth, 'message received'); + t2.ok(engine.inputNode.connectedFrom.length, 'engine connected'); + + effect2.clear(); + + target.inputNode._test('move', 'message does move'); + t2.ok(!target.inputNode._result(), 'message sent'); + t2.equal(engine.inputNode._result().depth, 1, 'message received'); + t2.ok(engine.inputNode.connectedFrom.length, 'engine connected'); + + t2.end(); + }); + + t1.end(); + }); +}; + +testEffect(PanEffect, 3); +testEffect(PitchEffect, 0); +testEffect(VolumeEffect, 1);