Optimise progress timer callback.

This commit is contained in:
jcass 2016-04-04 05:14:45 +02:00
parent 1345357a5e
commit f43a9a7afa
4 changed files with 329 additions and 172 deletions

View File

@ -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

View File

@ -1,6 +1,6 @@
CACHE MANIFEST
# 2016-04-03:v2
# 2016-04-06:v1
NETWORK:
*

View File

@ -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.

View File

@ -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(
'<div id="slidercontainer"><!-- slider for track position -->' +
@ -24,26 +38,40 @@ describe('SyncedTimer', function () {
'</div>'
)
$('#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()
})
})
})