diff --git a/mopidy_musicbox_webclient/static/js/synced_timer.js b/mopidy_musicbox_webclient/static/js/synced_timer.js
index 25b2eb9..e66b0eb 100644
--- a/mopidy_musicbox_webclient/static/js/synced_timer.js
+++ b/mopidy_musicbox_webclient/static/js/synced_timer.js
@@ -34,8 +34,8 @@
this.positionNode = document.createTextNode('')
this.durationNode = document.createTextNode('')
- $('#songelapsed').append(this.positionNode)
- $('#songlength').append(this.durationNode)
+ $('#songelapsed').empty().append(this.positionNode)
+ $('#songlength').empty().append(this.durationNode)
this._progressTimer = new ProgressTimer({
// Make sure that the timer object's context is available.
@@ -47,7 +47,12 @@
this._isConnected = false
this._mopidy.on('state:online', $.proxy(function () { this._isConnected = true }), this)
this._mopidy.on('state:offline', $.proxy(function () { this._isConnected = false }), this)
- this.init()
+ this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED
+ this._isSyncScheduled = false
+ this._scheduleID = null
+ this._syncAttemptsRemaining = this._maxAttempts
+ this._previousSyncPosition = null
+ this._duration = null
}
SyncedProgressTimer.SYNC_STATE = {
@@ -58,7 +63,7 @@
SyncedProgressTimer.format = function (milliseconds) {
if (milliseconds === Infinity) {
- return '(n/a)'
+ return ''
} else if (milliseconds === 0) {
return '0:00'
}
@@ -71,57 +76,37 @@
return minutes + ':' + seconds
}
- SyncedProgressTimer.prototype.init = function () {
- this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED
- this._syncAttemptsRemaining = this._maxAttempts
-
- this.positionNode.nodeValue = ''
- this.durationNode.nodeValue = ''
-
- this._scheduledSyncTime = null
- this._previousSyncPosition = null
- this._duration = null
-
- return this
- }
-
SyncedProgressTimer.prototype.timerCallback = function (position, duration) {
this._update(position, duration)
-
- if (this._isConnected && this._isSyncScheduled()) {
+ if (this._isSyncScheduled && this._isConnected) {
this._doSync(position, duration)
}
}
SyncedProgressTimer.prototype._update = function (position, duration) {
- if (!(duration === Infinity && position === 0)) {
- // Timer has been properly initialized.
- this.durationNode.nodeValue = SyncedProgressTimer.format(duration || Infinity)
- switch (this.syncState) {
- case SyncedProgressTimer.SYNC_STATE.NOT_SYNCED:
- // Waiting for Mopidy to provide a target position.
- this.positionNode.nodeValue = '(wait)'
- break
- case SyncedProgressTimer.SYNC_STATE.SYNCING:
- // Busy seeking to new target position.
- this.positionNode.nodeValue = '(sync)'
- break
- case SyncedProgressTimer.SYNC_STATE.SYNCED:
- this._previousSyncPosition = position
- this.positionNode.nodeValue = SyncedProgressTimer.format(position)
- $('#trackslider').val(position).slider('refresh')
- break
- }
- } else {
- // Make sure that default values are displayed while the timer is being initialized.
- this.positionNode.nodeValue = ''
- this.durationNode.nodeValue = ''
- $('#trackslider').val(0).slider('refresh')
+ switch (this.syncState) {
+ case SyncedProgressTimer.SYNC_STATE.NOT_SYNCED:
+ // Waiting for Mopidy to provide a target position.
+ this.positionNode.nodeValue = '(wait)'
+ break
+ case SyncedProgressTimer.SYNC_STATE.SYNCING:
+ // Busy seeking to new target position.
+ this.positionNode.nodeValue = '(sync)'
+ break
+ case SyncedProgressTimer.SYNC_STATE.SYNCED:
+ this._previousSyncPosition = position
+ this.positionNode.nodeValue = SyncedProgressTimer.format(position)
+ $('#trackslider').val(position).slider('refresh')
+ break
}
}
- SyncedProgressTimer.prototype._isSyncScheduled = function () {
- return this._scheduledSyncTime !== null && this._scheduledSyncTime <= new Date().getTime()
+ SyncedProgressTimer.prototype._scheduleSync = function (milliseconds) {
+ // Use an anonymous callback to set a boolean value, which should be faster to
+ // check in the timeout callback than doing another function call.
+ clearTimeout(this._scheduleID)
+ this._isSyncScheduled = false
+ this._scheduleID = setTimeout($.proxy(function () { this._isSyncScheduled = true }, this), milliseconds)
}
SyncedProgressTimer.prototype._doSync = function (position, duration) {
@@ -130,11 +115,14 @@
// Don't try to sync if progress timer has not been initialized yet.
return
}
+
var _this = this
_this._mopidy.playback.getTimePosition().then(function (targetPosition) {
+ if (_this.syncState === SyncedProgressTimer.SYNC_STATE.NOT_SYNCED) {
+ _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING
+ }
if (Math.abs(targetPosition - position) <= 500) {
// Less than 500ms == in sync.
- _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING
_this._syncAttemptsRemaining = Math.max(_this._syncAttemptsRemaining - 1, 0)
if (_this._syncAttemptsRemaining < _this._maxAttempts - 1 && _this._previousSyncPosition !== targetPosition) {
// Need at least two consecutive syncs to know that Mopidy
@@ -143,14 +131,13 @@
}
_this._previousSyncPosition = targetPosition
// Step back exponentially while increasing number of callbacks.
- _this._scheduledSyncTime = new Date().getTime() +
- delay_exponential(0.25, 2, _this._maxAttempts - _this._syncAttemptsRemaining) * 1000
+ _this._scheduleSync(delay_exponential(0.25, 2, _this._maxAttempts - _this._syncAttemptsRemaining) * 1000)
} else {
// Drift is too large, re-sync with Mopidy.
- _this._syncAttemptsRemaining = _this._maxAttempts
- _this._scheduledSyncTime = new Date().getTime() + 100
- _this._previousSyncPosition = null
_this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING
+ _this._syncAttemptsRemaining = _this._maxAttempts
+ _this._previousSyncPosition = null
+ _this._scheduleSync(1000)
_this._progressTimer.set(targetPosition)
}
})
@@ -160,58 +147,61 @@
if (arguments.length === 0) {
throw new Error('"SyncedProgressTimer.set" requires the "position" argument.')
}
+
+ this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED
+ this._syncAttemptsRemaining = this._maxAttempts
// Workaround for https://github.com/adamcik/media-progress-timer/issues/3
// This causes the timer to die unexpectedly if the position exceeds
// the duration slightly.
if (this._duration && this._duration < position) {
position = this._duration - 1
}
- this.init()
if (arguments.length === 1) {
this._progressTimer.set(position)
} else {
this._duration = duration
this._progressTimer.set(position, duration)
+ this.durationNode.nodeValue = SyncedProgressTimer.format(duration)
}
- if (!this._isSyncScheduled()) {
- // Set lapsed time and slider position directly as timer callback is not currently
- // running.
- this.positionNode.nodeValue = SyncedProgressTimer.format(position)
- if (arguments.length === 2) {
- this.durationNode.nodeValue = SyncedProgressTimer.format(duration)
- }
- $('#trackslider').val(position).slider('refresh')
- }
+ this.updatePosition(position, duration)
+ $('#trackslider').val(position).slider('refresh')
return this
}
SyncedProgressTimer.prototype.start = function () {
this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED
- this._scheduledSyncTime = new Date().getTime()
+ this._scheduleSync(0)
this._progressTimer.start()
return this
}
SyncedProgressTimer.prototype.stop = function () {
this._progressTimer.stop()
- this._scheduledSyncTime = null
- this.updatePosition(this._previousSyncPosition)
+ clearTimeout(this._scheduleID)
+ this._isSyncScheduled = false
+ if (this.syncState !== SyncedProgressTimer.SYNC_STATE.SYNCED && this._previousSyncPosition) {
+ // Timer was busy trying to sync when it was stopped, fallback to displaying the last synced position on screen.
+ this.positionNode.nodeValue = SyncedProgressTimer.format(this._previousSyncPosition)
+ }
return this
}
SyncedProgressTimer.prototype.reset = function () {
- this._progressTimer.reset()
+ // this._progressTimer.reset()
this.stop()
- this.init()
this.set(0, Infinity)
return this
}
SyncedProgressTimer.prototype.updatePosition = function (position) {
- this.positionNode.nodeValue = SyncedProgressTimer.format(position)
+ if (!(this._duration === Infinity && position === 0)) {
+ this.positionNode.nodeValue = SyncedProgressTimer.format(position)
+ } else {
+ this.positionNode.nodeValue = ''
+ }
}
return SyncedProgressTimer
diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache
index f21d33f..720bf20 100644
--- a/mopidy_musicbox_webclient/static/mb.appcache
+++ b/mopidy_musicbox_webclient/static/mb.appcache
@@ -1,6 +1,6 @@
CACHE MANIFEST
-# 2016-04-03:v2
+# 2016-04-06:v1
NETWORK:
*
diff --git a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js
index 4592fc0..59145ca 100644
--- a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js
+++ b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js
@@ -15,9 +15,15 @@
'use strict';
// Helper function to provide a reference time in milliseconds.
- var now = typeof window.performance !== 'undefined' &&
- typeof window.performance.now !== 'undefined' &&
- window.performance.now.bind(window.performance) || Date.now ||
+ var now = /* Sinon does not currently support faking `window.performance`
+ (see https://github.com/sinonjs/sinon/issues/803).
+ Changing this to only rely on `new Date().getTime()
+ in the interim in order to allow testing of the
+ progress timer from MMW.
+
+ typeof window.performance !== 'undefined' &&
+ typeof window.performance.now !== 'undefined' &&
+ window.performance.now.bind(window.performance) || Date.now ||*/
function() { return new Date().getTime(); };
// Helper to warn library users about deprecated features etc.
diff --git a/tests/js/test_synced_timer.js b/tests/js/test_synced_timer.js
index 458c294..1ab85f6 100644
--- a/tests/js/test_synced_timer.js
+++ b/tests/js/test_synced_timer.js
@@ -14,6 +14,20 @@ describe('SyncedTimer', function () {
var mopidy
var playback
var getTimePositionStub
+ var clock
+
+ function setFakeTimers () {
+ clock = sinon.useFakeTimers()
+ syncedProgressTimer._progressTimer = new ProgressTimer({
+ callback: $.proxy(syncedProgressTimer.timerCallback, syncedProgressTimer),
+ disableRequestAnimationFrame: true // No window available during testing - use fallback mechanism to schedule updates
+ })
+ }
+
+ function restoreFakeTimers () {
+ clock.restore()
+ }
+
before(function () {
$(document.body).append(
'
' +
@@ -24,26 +38,40 @@ describe('SyncedTimer', function () {
'
'
)
$('#trackslider').slider() // Initialize slider
- })
- beforeEach(function () {
+ $('#trackslider').on('slidestart', function () {
+ syncedProgressTimer.stop()
+ $('#trackslider').on('change', function () { syncedProgressTimer.updatePosition($(this).val()) })
+ })
+
+ $('#trackslider').on('slidestop', function () {
+ $('#trackslider').off('change')
+ syncedProgressTimer.updatePosition($(this).val())
+ // Simulate doSeekPos($(this).val())
+ syncedProgressTimer.set($(this).val())
+ })
+
playback = {
getTimePosition: function () { return $.when(1000) },
getState: function () { return $.when('stopped') }
}
- mopidy = new Mopidy({callingConvention: 'by-position-or-by-name'})
- mopidy.playback = playback
getTimePositionStub = sinon.stub(playback, 'getTimePosition')
- // Simulate Mopidy's track position advancing 10ms between each call.
- for (var i = 0; i < MAX_ATTEMPTS; i++) {
- getTimePositionStub.onCall(i).returns($.when(1000 + i * 10))
+ // Simulate Mopidy's track position advancing 250ms between each call for 0:01 to 0:10
+ for (var i = 0; i < 10000 / 250; i++) {
+ getTimePositionStub.onCall(i).returns($.when((i + 1) * 250))
}
- mopidy = sinon.stub(mopidy)
+ mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'}))
+ mopidy.playback = playback
+ })
+
+ beforeEach(function () {
syncedProgressTimer = new SyncedProgressTimer(MAX_ATTEMPTS, mopidy)
syncedProgressTimer._isConnected = true
})
+
afterEach(function () {
- getTimePositionStub.restore()
+ getTimePositionStub.reset()
})
+
describe('#SyncedTimer()', function () {
it('should add text nodes to DOM for position and duration indicators', function () {
expect($('#songelapsed')).to.have.text('')
@@ -61,7 +89,7 @@ describe('SyncedTimer', function () {
})
it('should handle Infinity', function () {
- assert.equal(SyncedProgressTimer.format(Infinity), '(n/a)')
+ assert.equal(SyncedProgressTimer.format(Infinity), '')
})
it('should handle zero', function () {
@@ -70,44 +98,57 @@ describe('SyncedTimer', function () {
})
describe('#timerCallback()', function () {
- var clock
beforeEach(function () {
- clock = sinon.useFakeTimers()
- syncedProgressTimer._progressTimer = new ProgressTimer({
- callback: $.proxy(syncedProgressTimer.timerCallback, syncedProgressTimer),
- disableRequestAnimationFrame: true // No window available during testing - use fallback mechanism to schedule updates
- })
+ setFakeTimers()
})
afterEach(function () {
- clock.restore()
+ restoreFakeTimers()
})
it('should not try to sync unless connected to mopidy', function () {
+ var _doSyncStub = sinon.stub(syncedProgressTimer, '_doSync')
+
syncedProgressTimer._isConnected = false
- var _syncScheduledStub = sinon.stub(syncedProgressTimer, '_isSyncScheduled')
- assert.isFalse(_syncScheduledStub.called, '_syncScheduledStub called')
- _syncScheduledStub.restore()
+ syncedProgressTimer.set(0, 1000).start()
+ clock.tick(1000)
+
+ assert.isFalse(_doSyncStub.called, '_doSync called')
+ syncedProgressTimer.stop()
+ _doSyncStub.restore()
})
it('should update text nodes', function () {
var updateStub = sinon.stub(syncedProgressTimer, '_update')
+
syncedProgressTimer.set(0, 1000).start()
assert.isTrue(updateStub.called, '_update not called')
+ syncedProgressTimer.stop()
updateStub.restore()
})
- it('should check if a sync is scheduled', function () {
- var scheduleStub = sinon.stub(syncedProgressTimer, '_isSyncScheduled').returns(true)
- syncedProgressTimer.set(0, 1000).start()
- assert.isTrue(scheduleStub.called, '_isSyncScheduled not called')
- scheduleStub.restore()
+ it('should attempt to perform a sync as soon as timer is started', function () {
+ var syncStub = sinon.stub(syncedProgressTimer, '_doSync')
+
+ syncedProgressTimer.set(0, 1000).start() // 'start' will immediately schedule a sync.
+ clock.tick(250)
+
+ assert.isTrue(syncStub.called, '_doSync not called')
+ syncedProgressTimer.stop()
+ syncStub.restore()
})
- it('should attempt to perform a sync when scheduled', function () {
+ it('should not attempt to perform a sync untill scheduled', function () {
var syncStub = sinon.stub(syncedProgressTimer, '_doSync')
- syncedProgressTimer.set(0, 1000).start()
+
+ syncedProgressTimer.set(0, 5000).start()
+ syncedProgressTimer._scheduleSync(500)
clock.tick(250)
- assert.isTrue(syncStub.called, '_doSync not called')
+ assert.isFalse(syncStub.called, 'next _doSync should only have been called after 500ms')
+
+ syncStub.reset()
+ clock.tick(500)
+ assert.isTrue(syncStub.called, 'next _doSync not called after 500ms')
+ syncedProgressTimer.stop()
syncStub.restore()
})
@@ -115,34 +156,27 @@ describe('SyncedTimer', function () {
// Simulate runtime on a 5-second track
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED, 'Timer was initialized in incorrect state')
syncedProgressTimer.set(0, 5000).start()
+
var wasSyncing = false
- for (var i = 0; i < MAX_ATTEMPTS; i++) {
- clock.tick(250) // 250ms * MAX_ATTEMPTS is only 2 seconds, but we'll be synced after only two attempts
+ for (var i = 0; i < 4; i++) {
+ clock.tick(250)
wasSyncing = wasSyncing || syncedProgressTimer.syncState === SyncedProgressTimer.SYNC_STATE.SYNCING
}
+ syncedProgressTimer.stop()
assert.isTrue(wasSyncing, 'Timer never entered the "syncing" state')
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCED, 'Timer failed to sync')
+ syncedProgressTimer.stop()
})
})
describe('#_update()', function () {
- it('should clear timers and reset slider to zero while not ready', function () {
- syncedProgressTimer.positionNode.nodeValue = '1:00'
- syncedProgressTimer.durationNode.nodeValue = '2:00'
- $('#trackslider').val(100).slider('refresh')
- syncedProgressTimer._update(0, Infinity)
-
- assert.equal(syncedProgressTimer.positionNode.nodeValue, '')
- assert.equal(syncedProgressTimer.durationNode.nodeValue, '')
- assert.equal($('#trackslider').val(), 0)
- })
-
- it('should set duration to "(n/a)" for tracks with infinite duration (e.g. streams)', function () {
+ it('should set duration to "" for tracks with infinite duration (e.g. streams)', function () {
syncedProgressTimer._update(1000, Infinity)
- assert.equal(syncedProgressTimer.durationNode.nodeValue, '(n/a)')
+ assert.equal(syncedProgressTimer.durationNode.nodeValue, '')
})
- it('should show "(wait)" while waiting for Mopidy to supply a position', function () {
+ it('should show "(wait)" while untill syncing starts', function () {
+ syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED
syncedProgressTimer._update(1000, 2000)
assert.equal(syncedProgressTimer.positionNode.nodeValue, '(wait)')
})
@@ -161,84 +195,122 @@ describe('SyncedTimer', function () {
})
})
- describe('#_isSyncScheduled()', function () {
- var scheduleSpy
- var clock
- before(function () {
- scheduleSpy = sinon.spy(syncedProgressTimer, '_isSyncScheduled')
- clock = sinon.useFakeTimers()
+ describe('#scheduleSync', function () {
+ beforeEach(function () {
+ setFakeTimers()
})
- after(function () {
- scheduleSpy.restore()
- clock.restore()
+ afterEach(function () {
+ restoreFakeTimers()
})
+
it('should schedule sync when scheduled time arrives', function () {
- syncedProgressTimer._scheduledSyncTime = new Date().getTime() + 1000
- assert.isFalse(syncedProgressTimer._isSyncScheduled())
- clock.tick(1000)
- assert.isTrue(syncedProgressTimer._isSyncScheduled())
+ clock.tick(0)
+ syncedProgressTimer._scheduleSync(1000)
+ assert.isFalse(syncedProgressTimer._isSyncScheduled)
+ clock.tick(1001)
+ assert.isTrue(syncedProgressTimer._isSyncScheduled)
+ })
+
+ it('should clear schedule on each call', function () {
+ var clearSpy = sinon.spy(window, 'clearTimeout')
+
+ clock.tick(0)
+ syncedProgressTimer._isSyncScheduled = true
+ syncedProgressTimer._scheduleSync(1000)
+ assert.isFalse(syncedProgressTimer._isSyncScheduled)
+
+ var scheduleID = syncedProgressTimer._scheduleID
+ clock.tick(1001)
+ syncedProgressTimer._scheduleSync(1000)
+ assert(clearSpy.calledWith(scheduleID))
+ clearSpy.restore()
})
})
describe('#_doSync', function () {
- var clock
beforeEach(function () {
- clock = sinon.useFakeTimers()
+ setFakeTimers()
})
afterEach(function () {
- clock.restore()
+ restoreFakeTimers()
})
+
it('should not try to sync until timer has been set', function () {
syncedProgressTimer._doSync(0, Infinity)
- assert.isFalse(getTimePositionStub.called, 'getTimePosition called even though timer has not been set')
+ assert.isFalse(getTimePositionStub.called, 'tried to do sync even though the timer has not been set')
})
+
it('should request position from Mopidy', function () {
syncedProgressTimer._doSync(1000, 2000)
assert.isTrue(getTimePositionStub.called, 'getTimePosition not called')
})
+ it('should set state to "SYNCING" as soon as the first sync attempt is made', function () {
+ syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED
+ syncedProgressTimer._doSync(100, 2000)
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
+ })
+
it('should set state to synced after two consecutive successful syncs (i.e. time drift < 500ms)', function () {
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED)
- clock.tick(10)
- syncedProgressTimer._doSync(1010, 2000)
+ clock.tick(250)
+ syncedProgressTimer._doSync(250, 2000)
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
- clock.tick(10)
- syncedProgressTimer._doSync(1020, 2000)
+ clock.tick(250)
+ syncedProgressTimer._doSync(500, 2000)
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCED)
})
it('should re-initialize and set state to syncing if time drift is more than 500ms', function () {
- syncedProgressTimer._doSync(1, 2000)
+ var scheduleStub = sinon.stub(syncedProgressTimer, '_scheduleSync')
+
+ syncedProgressTimer._doSync(1000, 2000)
+
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
assert.equal(syncedProgressTimer._syncAttemptsRemaining, syncedProgressTimer._maxAttempts)
+ assert.isNull(syncedProgressTimer._previousSyncPosition)
+ assert(scheduleStub.calledWith(1000), 'Expected next sync to be scheduled 1s from now')
+ scheduleStub.restore()
})
it('should step back exponentially while syncing', function () {
+ var scheduleStub = sinon.stub(syncedProgressTimer, '_scheduleSync')
+
for (var i = 0; i < syncedProgressTimer._maxAttempts; i++) {
- syncedProgressTimer._doSync(1000 + (i + 1) * 10, 2000)
- // If we don't advance the clock then '_syncAttemptsRemaining' should just contain the step-back in seconds
- assert.equal(syncedProgressTimer._syncAttemptsRemaining, syncedProgressTimer._maxAttempts - i - 1, 'Incorrect sync attempts remaining')
- assert.equal(syncedProgressTimer._scheduledSyncTime, (0.25 * (Math.pow(2, i)) * 1000), 'Incorrect sync time scheduled')
+ syncedProgressTimer._doSync(i * 250, 2000)
+ assert.equal(syncedProgressTimer._syncAttemptsRemaining, syncedProgressTimer._maxAttempts - i - 1, 'Incorrect number of sync attempts remaining')
+ assert(scheduleStub.calledWith(0.25 * (Math.pow(2, i)) * 1000), 'Incorrect sync time scheduled: ' + scheduleStub.getCall(i))
+ scheduleStub.reset()
}
+ scheduleStub.restore()
})
it('should check sync every 32s once synced', function () {
+ var scheduleStub = sinon.stub(syncedProgressTimer, '_scheduleSync')
+
syncedProgressTimer._syncAttemptsRemaining = 0
- syncedProgressTimer._doSync(1000, 2000)
- assert.equal(syncedProgressTimer._scheduledSyncTime, 32000)
+ syncedProgressTimer._doSync(250, 2000)
+ assert(scheduleStub.calledWith(32000))
+ scheduleStub.restore()
})
it('should not sync unless track playback is progressing', function () {
getTimePositionStub.restore()
- getTimePositionStub = sinon.stub(playback, 'getTimePosition')
- getTimePositionStub.returns($.when(1000)) // Simulate playback 'stuck' at 1000ms.
+
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED)
- clock.tick(10)
- syncedProgressTimer._doSync(1010, 2000)
+ clock.tick(250)
+ syncedProgressTimer._doSync(250, 2000)
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
- clock.tick(10)
- syncedProgressTimer._doSync(1010, 2000)
+ clock.tick(250)
+ syncedProgressTimer._doSync(250, 2000)
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
+
+ // Restore getTimePositionStub to previous state
+ getTimePositionStub = sinon.stub(playback, 'getTimePosition')
+ // Simulate Mopidy's track position advancing 250ms between each call for 0:01 to 0:10
+ for (var i = 0; i < 10000 / 250; i++) {
+ getTimePositionStub.onCall(i).returns($.when((i + 1) * 250))
+ }
})
})
@@ -252,18 +324,21 @@ describe('SyncedTimer', function () {
assert.equal(syncedProgressTimer._progressTimer._state.position, 1000)
})
- it('should update track slider if no sync is scheduled', function () {
+ it('should update position and track slider immediately', function () {
syncedProgressTimer.stop()
syncedProgressTimer.set(1000, 2000)
- expect($('#songelapsed').text()).to.endWith('0:01')
+
+ expect($('#songelapsed').text()).to.equal('0:01')
assert.equal($('#trackslider').val(), 1000)
})
it('should implement workaround for https://github.com/adamcik/media-progress-timer/issues/3', function () {
syncedProgressTimer.set(1000, 2000).start()
+
assert.equal(syncedProgressTimer._duration, 2000)
syncedProgressTimer.set(3000)
- assert.equal(syncedProgressTimer._progressTimer._state.position, 1999)
+ assert.equal(syncedProgressTimer._progressTimer._state.position, 1999, 'Expected position to be less than duration')
+ syncedProgressTimer.stop()
})
})
@@ -272,6 +347,7 @@ describe('SyncedTimer', function () {
var startStub = sinon.stub(syncedProgressTimer._progressTimer, 'start')
syncedProgressTimer.start()
assert(startStub.called)
+ syncedProgressTimer.stop()
startStub.restore()
})
@@ -279,16 +355,19 @@ describe('SyncedTimer', function () {
syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
syncedProgressTimer.start()
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED)
+ syncedProgressTimer.stop()
})
it('should schedule a sync immediately', function () {
- var clock = sinon.useFakeTimers()
- syncedProgressTimer._scheduledSyncTime = new Date().getTime() + 5000
- expect(syncedProgressTimer._scheduledSyncTime).to.be.above(new Date().getTime())
+ var scheduleSpy = sinon.spy(syncedProgressTimer, '_scheduleSync')
+
+ syncedProgressTimer.set(0, 1000)
+ syncedProgressTimer._isSyncScheduled = false
syncedProgressTimer.start()
- clock.tick(1000)
- expect(syncedProgressTimer._scheduledSyncTime).to.be.below(new Date().getTime())
- clock.restore()
+
+ assert(scheduleSpy.calledWith(0))
+ syncedProgressTimer.stop()
+ scheduleSpy.restore()
})
})
@@ -296,45 +375,51 @@ describe('SyncedTimer', function () {
it('should stop timer', function () {
var stopStub = sinon.stub(syncedProgressTimer._progressTimer, 'stop')
syncedProgressTimer.stop()
+
assert(stopStub.called)
+ syncedProgressTimer.stop()
stopStub.restore()
})
- it('should show position when stopped', function () {
+ it('should show last synced position if stopped while busy syncing', function () {
+ syncedProgressTimer.set(1000, 5000)
syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
- syncedProgressTimer._update(1000, 5000)
+ syncedProgressTimer._previousSyncPosition = 1000
syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING
syncedProgressTimer._update(2000, 5000)
assert.equal(syncedProgressTimer.positionNode.nodeValue, '(sync)')
syncedProgressTimer.stop()
assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:01')
+ expect($('#songelapsed').text()).to.equal('0:01')
})
it('should cancel any scheduled syncs', function () {
- syncedProgressTimer._scheduledSyncTime = 5000
+ var cancelSpy = sinon.spy(window, 'clearTimeout')
+
+ syncedProgressTimer._isSyncScheduled = true
syncedProgressTimer.stop()
- expect(syncedProgressTimer._scheduledSyncTime).to.be.null
+
+ assert.isFalse(syncedProgressTimer._isSyncScheduled)
+ assert(cancelSpy.calledWith(syncedProgressTimer._scheduleID))
+ cancelSpy.restore()
})
})
describe('#reset()', function () {
- it('should reset timer to 0:00 - (n/a) ', function () {
- var resetStub = sinon.stub(syncedProgressTimer._progressTimer, 'reset')
- var initStub = sinon.stub(syncedProgressTimer, 'init')
+ it('should reset timer to "" - "" ', function () {
var stopStub = sinon.stub(syncedProgressTimer, 'stop')
+ var setStub = sinon.stub(syncedProgressTimer, 'set')
syncedProgressTimer.reset()
- assert(resetStub.called)
- assert(initStub.called)
assert(stopStub.called)
+ assert(setStub.called)
- assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:00')
- assert.equal(syncedProgressTimer.durationNode.nodeValue, '(n/a)')
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '', 'Position node was not reset')
+ assert.equal(syncedProgressTimer.durationNode.nodeValue, '', 'Duration node was not reset')
- resetStub.restore()
- initStub.restore()
stopStub.restore()
+ setStub.restore()
})
})
@@ -345,8 +430,84 @@ describe('SyncedTimer', function () {
syncedProgressTimer.updatePosition(1000)
assert.isTrue(formatSpy.called)
- expect(syncedProgressTimer.positionNode.nodeValue).to.endWith('0:01')
+ expect(syncedProgressTimer.positionNode.nodeValue).to.equal('0:01')
formatSpy.restore()
})
+
+ it('should set position to "" if timer has not been initialized', function () {
+ syncedProgressTimer.set(1000, 2000)
+ expect(syncedProgressTimer.positionNode.nodeValue).to.equal('0:01')
+
+ syncedProgressTimer.updatePosition(0)
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:00', 'Position node was not reset')
+
+ syncedProgressTimer.reset()
+ syncedProgressTimer.updatePosition(0)
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '', 'Position node was not reset')
+ })
+ })
+
+ describe('integration tests', function () {
+ beforeEach(function () {
+ setFakeTimers()
+ })
+ afterEach(function () {
+ restoreFakeTimers()
+ })
+
+ it('simulate 30-second test run, ', function () {
+ // Initialize
+ syncedProgressTimer.reset()
+ expect($('#songelapsed').text()).to.equal('')
+ expect($('#songlength').text()).to.equal('')
+ assert.equal($('#trackslider').val(), 0)
+
+ // Set song info
+ syncedProgressTimer.set(0, 30000)
+ expect($('#songelapsed').text()).to.equal('0:00')
+ expect($('#songlength').text()).to.equal('0:30')
+ assert.equal($('#trackslider').val(), 0)
+
+ // Start
+ syncedProgressTimer.start()
+ clock.tick(40)
+ expect($('#songelapsed').text()).to.equal('(wait)')
+ expect($('#songlength').text()).to.equal('0:30')
+ assert.equal($('#trackslider').val(), 0)
+
+ // Syncing
+ clock.tick(250)
+ expect($('#songelapsed').text()).to.equal('(sync)')
+ expect($('#songlength').text()).to.equal('0:30')
+ assert.equal($('#trackslider').val(), 0)
+
+ // Synced
+ clock.tick(1000)
+ expect($('#songelapsed').text()).to.equal('0:01')
+ expect($('#songlength').text()).to.equal('0:30')
+ assert.isAtLeast($('#trackslider').val(), 1000)
+
+ // Move slider
+ $('#trackslider').trigger('slidestart')
+ clock.tick(250)
+ $('#trackslider').val(5000).slider('refresh')
+ $('#trackslider').trigger('change')
+ clock.tick(250)
+ $('#trackslider').trigger('slidestop')
+
+ clock.tick(1000) // Position should remain '0:05' as the timer should not be running after a slider change
+ expect($('#songelapsed').text()).to.equal('0:05')
+
+ // Start -> Sync -> Stop
+ syncedProgressTimer.start()
+ clock.tick(40)
+ expect($('#songelapsed').text()).to.equal('(sync)')
+ syncedProgressTimer._previousSyncPosition = 1000
+ syncedProgressTimer.stop()
+ expect($('#songelapsed').text()).to.equal('0:01')
+ expect($('#songlength').text()).to.equal('0:30')
+
+ syncedProgressTimer.stop()
+ })
})
})