Optimise progress timer callback.
This commit is contained in:
parent
1345357a5e
commit
f43a9a7afa
@ -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,32 +76,14 @@
|
||||
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.
|
||||
@ -112,16 +99,14 @@
|
||||
$('#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')
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!(this._duration === Infinity && position === 0)) {
|
||||
this.positionNode.nodeValue = SyncedProgressTimer.format(position)
|
||||
} else {
|
||||
this.positionNode.nodeValue = ''
|
||||
}
|
||||
}
|
||||
|
||||
return SyncedProgressTimer
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
CACHE MANIFEST
|
||||
|
||||
# 2016-04-03:v2
|
||||
# 2016-04-06:v1
|
||||
|
||||
NETWORK:
|
||||
*
|
||||
|
||||
@ -15,9 +15,15 @@
|
||||
'use strict';
|
||||
|
||||
// Helper function to provide a reference time in milliseconds.
|
||||
var now = typeof window.performance !== 'undefined' &&
|
||||
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 ||
|
||||
window.performance.now.bind(window.performance) || Date.now ||*/
|
||||
function() { return new Date().getTime(); };
|
||||
|
||||
// Helper to warn library users about deprecated features etc.
|
||||
|
||||
@ -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
|
||||
$('#trackslider').on('slidestart', function () {
|
||||
syncedProgressTimer.stop()
|
||||
$('#trackslider').on('change', function () { syncedProgressTimer.updatePosition($(this).val()) })
|
||||
})
|
||||
beforeEach(function () {
|
||||
|
||||
$('#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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user