Merge pull request #189 from jcass77/enhance/progress_timer_tests

Upgrade media progress timer. Modularise progress_timer.js.
This commit is contained in:
John Cass 2016-04-03 15:09:50 +02:00
commit 1d4db1782a
17 changed files with 1253 additions and 841 deletions

View File

@ -77,6 +77,7 @@ v2.3.0 (UNRELEASED)
- Now displays album and artist info when browsing tracks. (Addresses: `#99 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/99>`_).
- Now remembers which backend was searched previously, and automatically selects that backend as the default search target.
(Addresses: `#130 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/130>`_).
- Upgrade Media Progress Timer to version 3.0.0.
**Fixes**

View File

@ -13,7 +13,6 @@ module.exports = function (config) {
// list of files / patterns to load in the browser
files: [
'mopidy_musicbox_webclient/static/vendors/**/*.js',
// TODO: can remove the next line once JavaScript codebase has been modularized.
'mopidy_musicbox_webclient/static/js/**/*.js',
'tests/**/test_*.js'
],

View File

@ -486,7 +486,7 @@
<!-- /page one -->
<script type="text/javascript" src="vendors/mopidy/mopidy.min.js"></script>
<script type="text/javascript" src="vendors/media_progress_timer/timer.js"></script>
<script type="text/javascript" src="js/progress_timer.js"></script>
<script type="text/javascript" src="js/synced_timer.js"></script>
<script type="text/javascript" src="js/controls.js"></script>
<script type="text/javascript" src="js/library.js"></script>
<script type="text/javascript" src="js/functionsvars.js"></script>

View File

@ -288,13 +288,13 @@ function setPlayState (nwplay) {
$('#btplay >i').removeClass('fa-play').addClass('fa-pause')
$('#btplay').attr('title', 'Pause')
mopidy.playback.getTimePosition().then(processCurrentposition, console.error)
startProgressTimer()
syncedProgressTimer.start()
} else {
$('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play')
$('#btplayNowPlaying').attr('title', 'Play')
$('#btplay >i').removeClass('fa-pause').addClass('fa-play')
$('#btplay').attr('title', 'Play')
progressTimer.stop()
syncedProgressTimer.stop()
}
play = nwplay
}
@ -396,7 +396,7 @@ function doSeekPos (value) {
function setPosition (pos) {
if (!positionChanging && $('#trackslider').val() !== pos) {
setProgressTimer(pos)
syncedProgressTimer.set(pos)
}
}

View File

@ -5,6 +5,7 @@
*/
var mopidy
var syncedProgressTimer
// values for controls
var play = false
@ -268,7 +269,7 @@ function renderSongLiDivider (track, nextTrack, currentIndex, target) {
renderSongLiTrackArtists(track) + '</p></a></li>'
)
// Retrieve album covers
coverArt.getCover(track.uri, getjQueryID(target + '-cover', track.uri, true), 'small')
coverArt.getCover(track.uri, getjQueryID(target + '-cover', track.uri, true), 'small', mopidy)
} else if (currentIndex > 0) {
// Small divider
$(target).before('<li class="smalldivider"> &nbsp;</li>')

View File

@ -128,7 +128,7 @@ function setSongInfo (data) {
}
if (data.track.album && data.track.album.name) {
$('#modalalbum').html('<a href="#" onclick="return library.showAlbum(\'' + data.track.album.uri + '\');">' + data.track.album.name + '</a>')
coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge')
coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge', mopidy)
} else {
$('#modalalbum').html('')
$('#infocover').attr('src', 'images/default_cover.png')
@ -139,10 +139,9 @@ function setSongInfo (data) {
$('#trackslider').attr('min', 0)
$('#trackslider').attr('max', songlength)
resetProgressTimer()
progressTimer.set(0, songlength)
syncedProgressTimer.reset().set(0, songlength)
if (play) {
startProgressTimer()
syncedProgressTimer.start()
}
resizeMb()
@ -239,7 +238,7 @@ function initSocketevents () {
showFavourites()
})
library.getBrowseDir()
library.getSearchSchemes()
library.getSearchSchemes(searchBlacklist, mopidy)
showLoading(false)
$(window).hashchange()
})
@ -302,7 +301,7 @@ function initSocketevents () {
mopidy.on('event:seeked', function (data) {
setPosition(parseInt(data.time_position))
if (play) {
startProgressTimer()
syncedProgressTimer.start()
}
})
@ -496,9 +495,7 @@ $(document).ready(function (event) {
// initialize events
initSocketevents()
progressTimer = new ProgressTimer({
callback: timerCallback
})
syncedProgressTimer = new SyncedProgressTimer(8, mopidy)
resetSong()
@ -593,13 +590,13 @@ $(document).ready(function (event) {
// swipe songinfo and panel
$('#normalFooter, #nowPlayingFooter').on('swiperight', doPrevious)
$('#normalFooter, #nowPlayingFooter').on('swipeleft', doNext)
$('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swiperight', function () {
$('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swiperight', function (event) {
if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) {
$('#panel').panel('open')
event.stopImmediatePropagation()
}
})
$('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swipeleft', function () {
$('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swipeleft', function (event) {
if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) {
$('#panel').panel('close')
event.stopImmediatePropagation()
@ -607,12 +604,13 @@ $(document).ready(function (event) {
})
$('#trackslider').on('slidestart', function () {
progressTimer.stop()
$('#trackslider').on('change', function () { updatePosition($(this).val()) })
syncedProgressTimer.stop()
$('#trackslider').on('change', function () { syncedProgressTimer.updatePosition($(this).val()) })
})
$('#trackslider').on('slidestop', function () {
$('#trackslider').off('change')
syncedProgressTimer.updatePosition($(this).val())
doSeekPos($(this).val())
})

View File

@ -1,19 +1,25 @@
/**
* @author Wouter van Wijk
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory)
} else if (typeof module === 'object' && module.exports) {
module.exports = factory()
} else {
root.coverArt = factory()
}
}(this, function () {
'use strict'
API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733'
API_SECRET = '2c631802c2285d5d5d1502462fe42a2b'
var API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733'
var API_SECRET = '2c631802c2285d5d5d1502462fe42a2b'
var coverArt = {
fmcache: new LastFMCache(),
lastfm: new LastFM({
apiKey: API_KEY,
apiSecret: API_SECRET,
cache: this.fmcache
cache: new LastFMCache()
}),
getCover: function (uri, images, size) {
getCover: function (uri, images, size, mopidy) {
var defUrl = 'images/default_cover.png'
$(images).attr('src', defUrl)
if (!uri) {
@ -26,7 +32,7 @@ var coverArt = {
} else {
// Also check deprecated 'album.images' in case backend does not
// implement mopidy.library.getImages yet...
coverArt.getCoverFromAlbum(uri, images, size)
coverArt.getCoverFromAlbum(uri, images, size, mopidy)
}
})
},
@ -34,7 +40,7 @@ var coverArt = {
// Note that this approach has been deprecated in Mopidy
// TODO: Remove when Mopidy no longer supports getting images
// with 'album.images'.
getCoverFromAlbum: function (uri, images, size) {
getCoverFromAlbum: function (uri, images, size, mopidy) {
var defUrl = 'images/default_cover.png'
$(images).attr('src', defUrl)
if (!uri) {
@ -98,13 +104,5 @@ var coverArt = {
}})
}
}
$(document).ready(coverArt.init)
// TODO: Remove this once JavaScript codebase has been completely modularized
// in favour of bundling everything using 'browserify'.
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
module.exports = coverArt
}
}
return coverArt
}))

View File

@ -1,3 +1,14 @@
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory)
} else if (typeof module === 'object' && module.exports) {
module.exports = factory()
} else {
root.library = factory()
}
}(this, function () {
'use strict'
var library = {
/** *******************************
@ -313,7 +324,7 @@ var library = {
return false
},
getSearchSchemes: function () {
getSearchSchemes: function (searchBlacklist, mopidy) {
var backendName
var searchScheme = $.cookie('searchScheme')
if (searchScheme) {
@ -337,11 +348,5 @@ var library = {
}, console.error)
}
}
// TODO: Remove this once JavaScript codebase has been completely modularized
// in favour of bundling everything using 'browserify'.
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
module.exports = library
}
}
return library
}))

View File

@ -220,7 +220,7 @@ function processCurrentPlaylist (resultArr) {
function processArtistResults (resultArr) {
if (!resultArr || (resultArr.length === 0)) {
$('#h_artistname').text('Artist not found...')
coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge')
coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge', mopidy)
showLoading(false)
return
}
@ -239,7 +239,7 @@ function processArtistResults (resultArr) {
function processAlbumResults (resultArr) {
if (!resultArr || (resultArr.length === 0)) {
$('#h_albumname').text('Album not found...')
coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge')
coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge', mopidy)
showLoading(false)
return
}

View File

@ -1,177 +0,0 @@
var progressTimer
var positionNode = document.createTextNode('')
var durationNode = document.createTextNode('')
var START_BEATS = 5 // 0.5 seconds, needs to be less than 1s to prevent unwanted updates.
var RUN_BEATS = 300 // 30 seconds assuming default timer update rate of 100ms
var callbackHeartbeats = 0 // Timer will check syncs on every n-number of calls.
var targetPosition = null
var MAX_SYNCS = 5 // Maximum number of consecutive successful syncs to perform.
var syncsLeft = MAX_SYNCS
var synced = false
var consecutiveSyncs = 0
$(document).ready(function () {
$('#songelapsed').append(positionNode)
$('#songlength').append(durationNode)
})
function timerCallback (position, duration, isRunning) {
updateTimers(position, duration, isRunning)
if (callbackHeartbeats === 0) {
callbackHeartbeats = getHeartbeat()
}
if (mopidy && position > 0) {
// Mopidy and timer are both initialized.
if (callbackHeartbeats-- === 1) {
// Get time position from Mopidy on every nth callback until
// synced.
mopidy.playback.getTimePosition().then(
function (mopidy_position) {
syncTimer(position, mopidy_position)
}
)
}
}
}
function updateTimers (position, duration, isRunning) {
var ready = !(duration === Infinity && position === 0 && !isRunning) // Timer has been properly initialized.
var streaming = (duration === Infinity && position > 0) // Playing a stream.
var ok = synced && isRunning // Normal operation.
var syncing = !synced && isRunning // Busy syncing.
if (!ready) {
// Make sure that default values are displayed while the timer is being initialized.
positionNode.nodeValue = ''
durationNode.nodeValue = ''
$('#trackslider').val(0).slider('refresh')
} else {
durationNode.nodeValue = format(duration || Infinity)
if (syncing) {
if (!targetPosition) {
// Waiting for Mopidy to provide a target position.
positionNode.nodeValue = '(wait)'
} else {
// Busy seeking to new target position.
positionNode.nodeValue = '(sync)'
}
} else if (synced || streaming) {
positionNode.nodeValue = format(position)
}
}
if (ok) {
// Don't update the track slider unless it is synced and running.
// (prevents awkward 'jitter' animation).
$('#trackslider').val(position).slider('refresh')
}
}
function getHeartbeat () {
if (syncsLeft > 0 && callbackHeartbeats === 0) {
// Step back exponentially while increasing heartbeat.
return Math.round(delay_exponential(5, 2, MAX_SYNCS - syncsLeft))
} else if (syncsLeft === 0 && callbackHeartbeats === 0) {
// Sync completed, keep checking using maximum number of heartbeats.
return RUN_BEATS
} else {
return START_BEATS
}
}
function syncTimer (current, target) {
if (target) {
var drift = Math.abs(target - current)
if (drift <= 500) {
syncsLeft--
// Less than 500ms == in sync.
if (++consecutiveSyncs === 2) {
// Need at least two consecutive syncs to know that Mopidy
// is progressing playback and we are in sync.
synced = true
targetPosition = null
consecutiveSyncs = 0
}
} else {
// Drift is too large, re-sync with Mopidy.
reset()
targetPosition = target
progressTimer.set(targetPosition)
}
}
}
function toInt (value) {
return value.match(/^\w*\d+\w*$/) ? parseInt(value) : null
}
function format (milliseconds) {
if (milliseconds === Infinity) {
return '(n/a)'
} else if (milliseconds === 0) {
return '0:00'
}
var seconds = Math.floor(milliseconds / 1000)
var minutes = Math.floor(seconds / 60)
seconds = seconds % 60
seconds = seconds < 10 ? '0' + seconds : seconds
return minutes + ':' + seconds
}
function delay_exponential (base, growthFactor, attempts) {
/* Calculate number of beats between syncs based on exponential function.
The format is::
base * growthFactor ^ (attempts - 1)
If ``base`` is set to 'rand' then a random number between
0 and 1 will be used as the base.
Base must be greater than 0.
*/
if (base === 'rand') {
base = Math.random()
}
beats = base * (Math.pow(growthFactor, (attempts - 1)))
return beats
}
function reset () {
synced = false
consecutiveSyncs = 0
syncsLeft = MAX_SYNCS
callbackHeartbeats = START_BEATS
targetPosition = null
}
function setProgressTimer (pos) {
reset()
targetPosition = pos
progressTimer.set(pos)
if (!play) {
// Set lapsed time and slider position directly as timer callback is not currently
// running.
positionNode.nodeValue = format(pos)
$('#trackslider').val(pos).slider('refresh')
}
}
function updatePosition (pos) {
positionNode.nodeValue = format(pos)
}
function startProgressTimer () {
reset()
progressTimer.start()
}
function resetProgressTimer () {
progressTimer.reset()
reset()
targetPosition = 0
}

View File

@ -0,0 +1,218 @@
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory)
} else if (typeof module === 'object' && module.exports) {
module.exports = factory()
} else {
root.SyncedProgressTimer = factory()
}
}(this, function () {
'use strict'
function delay_exponential (base, growthFactor, attempts) {
/* Calculate number of beats between syncs based on exponential function.
The format is::
base * growthFactor ^ (attempts - 1)
If ``base`` is set to 'rand' then a random number between
0 and 1 will be used as the base.
Base must be greater than 0.
*/
if (base === 'rand') {
base = Math.random()
}
// console.log(base + ' * (Math.pow(' + growthFactor + ', (' + attempts + ' - 1)) = ' + base * (Math.pow(growthFactor, (attempts - 1))))
return base * (Math.pow(growthFactor, (attempts - 1)))
}
function SyncedProgressTimer (maxAttempts, mopidy) {
if (!(this instanceof SyncedProgressTimer)) {
return new SyncedProgressTimer(maxAttempts, mopidy)
}
this.positionNode = document.createTextNode('')
this.durationNode = document.createTextNode('')
$('#songelapsed').append(this.positionNode)
$('#songlength').append(this.durationNode)
this._progressTimer = new ProgressTimer({
// Make sure that the timer object's context is available.
callback: $.proxy(this.timerCallback, this)
})
this._maxAttempts = maxAttempts
this._mopidy = mopidy
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()
}
SyncedProgressTimer.SYNC_STATE = {
NOT_SYNCED: 0,
SYNCING: 1,
SYNCED: 2
}
SyncedProgressTimer.format = function (milliseconds) {
if (milliseconds === Infinity) {
return '(n/a)'
} else if (milliseconds === 0) {
return '0:00'
}
var seconds = Math.floor(milliseconds / 1000)
var minutes = Math.floor(seconds / 60)
seconds = seconds % 60
seconds = seconds < 10 ? '0' + seconds : seconds
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()) {
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')
}
}
SyncedProgressTimer.prototype._isSyncScheduled = function () {
return this._scheduledSyncTime !== null && this._scheduledSyncTime <= new Date().getTime()
}
SyncedProgressTimer.prototype._doSync = function (position, duration) {
var ready = !(duration === Infinity && position === 0) // Timer has been properly initialized.
if (!ready) {
// 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 (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
// is progressing playback and we are in sync.
_this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
}
_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
} 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._progressTimer.set(targetPosition)
}
})
}
SyncedProgressTimer.prototype.set = function (position, duration) {
if (arguments.length === 0) {
throw new Error('"SyncedProgressTimer.set" requires the "position" argument.')
}
// 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)
}
$('#trackslider').val(position).slider('refresh')
}
return this
}
SyncedProgressTimer.prototype.start = function () {
this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED
this._scheduledSyncTime = new Date().getTime()
this._progressTimer.start()
return this
}
SyncedProgressTimer.prototype.stop = function () {
this._progressTimer.stop()
this._scheduledSyncTime = null
this.updatePosition(this._previousSyncPosition)
return this
}
SyncedProgressTimer.prototype.reset = function () {
this._progressTimer.reset()
this.stop()
this.init()
this.set(0, Infinity)
return this
}
SyncedProgressTimer.prototype.updatePosition = function (position) {
this.positionNode.nodeValue = SyncedProgressTimer.format(position)
}
return SyncedProgressTimer
}))

View File

@ -1,6 +1,6 @@
CACHE MANIFEST
# 2016-03-28:v1
# 2016-04-03:v1
NETWORK:
*
@ -26,7 +26,7 @@ js/gui.js
js/images.js
js/library.js
js/process_ws.js
js/progress_timer.js
js/synced_timer.js
mb.appcache
system.html
vendors/font_awesome/css/font-awesome.css

View File

@ -1,128 +1,159 @@
/*! timer.js v2.0.2
/*! timer.js v3.0.0
* https://github.com/adamcik/media-progress-timer
* Copyright (c) 2015 Thomas Adamcik
* Copyright (c) 2015-2016 Thomas Adamcik
* Licensed under the Apache License, Version 2.0 */
(function() {
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.ProgressTimer = factory();
}
}(this, function () {
'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 ||
function() { return new Date().getTime(); };
// Helper to warn library users about deprecated features etc.
function warn(msg) {
window.setTimeout(function() { throw msg; }, 0);
}
// Creates a new timer object, works with both 'new ProgressTimer(options)'
// and just 'ProgressTimer(options). Optionally the timer can also be
// called with only the callback instead of options.
function ProgressTimer(options) {
if (!(this instanceof ProgressTimer)) {
return new ProgressTimer(options);
} else if (typeof options === 'function') {
options = {'callback': options};
} else if (typeof options !== 'object') {
throw 'ProgressTimer must be called with a callback or options.';
} else if (typeof options.callback !== 'function') {
throw 'ProgressTimer needs a callback to operate.';
throw '"ProgressTimer" must be called with a callback or options.';
} else if (typeof options['callback'] !== 'function') {
throw '"ProgressTimer" needs a callback to operate.';
}
this._running = false;
this._updateRate = Math.max(options.updateRate || 100, 10);
this._callback = options.callback;
this._fallback = typeof window.requestAnimationFrame === 'undefined' ||
options.disableRequestAnimationFrame|| false;
this._userCallback = options['callback'];
this._updateId = null;
this._state = null; // Gets initialized by the set() call.
if (!this._fallback) {
this._callUpdate = this._scheduleAnimationFrame;
this._scheduleUpdate = this._scheduleAnimationFrame;
var frameDuration = 1000 / (options['fallbackTargetFrameRate'] || 30);
// TODO: Remove this legacy code path at some point.
if (options['updateRate'] && !options['fallbackTargetFrameRate']) {
warn('"ProgressTimer" no longer supports the updateRate option.');
frameDuration = Math.max(options['updateRate'], 1000 / 60);
}
this._boundCallUpdate = this._callUpdate.bind(this);
this._boundUpdate = this._update.bind(this);
var useFallback = (
typeof window.requestAnimationFrame === 'undefined' ||
typeof window.cancelAnimationFrame === 'undefined' ||
options['disableRequestAnimationFrame'] || false);
this.reset();
// Make sure this works in _update.
var update = this._update.bind(this);
if (useFallback) {
this._schedule = function(timestamp) {
var timeout = Math.max(timestamp + frameDuration - now(), 0);
return window.setTimeout(update, Math.floor(timeout));
};
this._cancel = window.clearTimeout.bind(window);
} else {
this._schedule = window.requestAnimationFrame.bind(window, update);
this._cancel = window.cancelAnimationFrame.bind(window);
}
this.reset(); // Reuse reset code to ensure we start in the same state.
}
// If called with one argument the previous duration is preserved. Note
// that the position can be changed while the timer is running.
ProgressTimer.prototype.set = function(position, duration) {
if (arguments.length === 0) {
throw 'set requires at least a position argument.';
throw '"ProgressTimer.set" requires the "position" arugment.';
} else if (arguments.length === 1) {
// Fallback to previous duration, whatever that was.
duration = this._state.duration;
} else {
duration = Math.floor(
Math.max(duration === null ? Infinity : duration || 0, 0));
// Round down and make sure zero and null are treated as inf.
duration = Math.floor(Math.max(
duration === null ? Infinity : duration || Infinity, 0));
}
// Make sure '0 <= position <= duration' always holds.
position = Math.floor(Math.min(Math.max(position || 0, 0), duration));
this._state = {
initialTimestamp: null,
initialPosition: position,
previousPosition: position,
position: position,
duration: duration
};
this._callback(position, duration, this._running);
return this;
};
ProgressTimer.prototype.start = function() {
this._running = true;
this._callUpdate();
return this;
};
ProgressTimer.prototype.stop = function() {
this._running = false;
var state = this._state;
return this.set(state.previousPosition, state.duration);
};
ProgressTimer.prototype.reset = function() {
this._running = false;
return this.set(0, Infinity);
};
ProgressTimer.prototype._callUpdate = function() {
this._update(now());
};
ProgressTimer.prototype._scheduleUpdate = function(timestamp) {
var adjustedTimeout = timestamp + this._updateRate - now();
setTimeout(this._boundCallUpdate, adjustedTimeout);
};
ProgressTimer.prototype._scheduleAnimationFrame = function() {
window.requestAnimationFrame(this._boundUpdate);
};
ProgressTimer.prototype._update = function(timestamp) {
if (!this._running) {
return;
// Update right away if we don't have anything running.
if (this._updateId === null) {
// TODO: Consider wrapping this in a try/catch?
this._userCallback(position, duration);
}
return this;
};
var state = this._state;
// Start the timer if it is not already running.
ProgressTimer.prototype.start = function() {
if (this._updateId === null) {
this._updateId = this._schedule(0);
}
return this;
};
// Cancel the timer if it us currently tracking progress.
ProgressTimer.prototype.stop = function() {
if (this._updateId !== null) {
this._cancel(this._updateId);
// Ensure we correctly reset the initial position and timestamp.
this.set(this._state.position, this._state.duration);
this._updateId = null; // Last step to avoid callback in set()
}
return this;
};
// Marks the timer as stopped, sets position to zero and duration to inf.
ProgressTimer.prototype.reset = function() {
return this.stop().set(0, Infinity);
};
// Calls the user callback with the current position/duration and then
// schedules the next update run via _schedule if we haven't finished.
ProgressTimer.prototype._update = function(timestamp) {
var state = this._state; // We refer a lot to state, this is shorter.
// Make sure setTimeout has a timestamp and store first reference time.
timestamp = timestamp || now();
state.initialTimestamp = state.initialTimestamp || timestamp;
var position = state.initialPosition + timestamp - state.initialTimestamp;
var duration = state.duration;
// Recalculate position according to start location and reference.
state.position = (
state.initialPosition + timestamp - state.initialTimestamp);
if (position < duration || duration === null) {
var delta = position - state.previousPosition;
if (delta >= this._updateRate || this._fallback) {
this._callback(Math.floor(position), duration, this._running);
state.previousPosition = position;
}
// Ensure callback gets an integer and that 'position <= duration'.
var userPosisition = Math.min(
Math.floor(state.position), state.duration);
// TODO: Consider wrapping this in a try/catch?
this._userCallback(userPosisition, state.duration);
if (state.position < state.duration) {
this._updateId = this._schedule(timestamp); // Schedule update.
} else {
// Workaround for https://github.com/adamcik/media-progress-timer/issues/3
// This causes the timer to die unexpectedly if the position goes
// over the duration slightly.
// this._running = false;
this._callback(duration, duration, this._running);
this._updateId = null; // Unset since we didn't reschedule.
}
this._scheduleUpdate(timestamp);
};
if(typeof module !== 'undefined') {
module.exports = ProgressTimer;
} else {
window.ProgressTimer = ProgressTimer;
}
}());
return ProgressTimer;
}));

View File

@ -1,5 +1,4 @@
var chai = require('chai')
var should = chai.should()
var expect = chai.expect
var assert = chai.assert
chai.use(require('chai-string'))
@ -7,24 +6,20 @@ chai.use(require('chai-jquery'))
var sinon = require('sinon')
var coverArt = require('../mopidy_musicbox_webclient/static/js/images.js')
var images
before(function () {
mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'}))
images = $('<img id="img_mock">')
})
var coverArt = require('../../mopidy_musicbox_webclient/static/js/images.js')
describe('CoverArt', function () {
describe('#getCover()', function () {
var mopidy
var images
beforeEach(function () {
mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'}))
images = $('<img id="img_mock">')
$(images).removeAttr('src')
})
describe('#getCover()', function () {
it('should use default image if no track URI is provided', function () {
coverArt.getCover('', images, '')
$(images).prop('src').should.endWith('images/default_cover.png')
coverArt.getCover('', images, '', mopidy)
expect($(images).prop('src')).to.endWith('images/default_cover.png')
})
it('should get image from Mopidy, if available', function () {
@ -33,10 +28,10 @@ describe('CoverArt', function () {
mopidy.library = library
var getImagesSpy = sinon.spy(mopidy.library, 'getImages')
coverArt.getCover('mock:track:uri', images, '')
coverArt.getCover('mock:track:uri', images, '', mopidy)
assert(getImagesSpy.calledOnce)
$(images).prop('src').should.endWith('mockImageUri')
assert.isTrue(getImagesSpy.calledOnce)
expect($(images).prop('src')).to.endWith('mockImageUri')
})
it('should fall back to retrieving image from deprecated track.album.images', function () {
@ -51,21 +46,17 @@ describe('CoverArt', function () {
var getImagesSpy = sinon.spy(mopidy.library, 'getImages')
var getCoverFromAlbumSpy = sinon.spy(coverArt, 'getCoverFromAlbum')
coverArt.getCover('mock:track:uri', images, '')
coverArt.getCover('mock:track:uri', images, '', mopidy)
assert(getImagesSpy.calledOnce)
assert(getCoverFromAlbumSpy.calledOnce)
assert.isTrue(getImagesSpy.calledOnce)
assert.isTrue(getCoverFromAlbumSpy.calledOnce)
})
})
describe('#getCoverFromAlbum()', function () {
beforeEach(function () {
$(images).removeAttr('src')
})
it('should use default image if no track URI is provided', function () {
coverArt.getCoverFromAlbum('', images, '')
$(images).prop('src').should.endWith('images/default_cover.png')
coverArt.getCoverFromAlbum('', images, '', mopidy)
expect($(images).prop('src')).to.endWith('images/default_cover.png')
})
it('should get image from Mopidy track.album.images, if available', function () {
@ -76,10 +67,10 @@ describe('CoverArt', function () {
mopidy.library = library
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
coverArt.getCoverFromAlbum('mock:track:uri', images, '')
coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy)
assert(lookupSpy.calledOnce)
$(images).prop('src').should.endWith('mockAlbumImageUri')
assert.isTrue(lookupSpy.calledOnce)
expect($(images).prop('src')).to.endWith('mockAlbumImageUri')
})
it('should use default image if track.album or track.artist is not available', function () {
@ -90,10 +81,10 @@ describe('CoverArt', function () {
mopidy.library = library
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
coverArt.getCoverFromAlbum('mock:track:uri', images, '')
coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy)
assert(lookupSpy.calledOnce)
$(images).prop('src').should.endWith('images/default_cover.png')
assert.isTrue(lookupSpy.calledOnce)
expect($(images).prop('src')).to.endWith('images/default_cover.png')
})
it('should fall back to retrieving image from last.fm if none provided by Mopidy', function () {
@ -103,21 +94,18 @@ describe('CoverArt', function () {
}
mopidy.library = library
var getCoverFromLastFmSpy = sinon.spy(coverArt, 'getCoverFromLastFm')
coverArt.getCoverFromAlbum('mock:track:uri', images, '')
var getCoverFromLastFmStub = sinon.stub(coverArt, 'getCoverFromLastFm')
coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy)
assert(getCoverFromLastFmSpy.calledOnce)
assert.isTrue(getCoverFromLastFmStub.calledOnce)
getCoverFromLastFmStub.restore()
})
})
describe('#getCoverFromLastFm()', function () {
beforeEach(function () {
$(images).removeAttr('src')
})
it('should use default image if no track is provided', function () {
coverArt.getCoverFromLastFm(undefined, images, '')
$(images).prop('src').should.endWith('images/default_cover.png')
expect($(images).prop('src')).to.endWith('images/default_cover.png')
})
it('should fall back to using track artist if album artist is not available', function () {
@ -129,7 +117,7 @@ describe('CoverArt', function () {
coverArt.getCoverFromLastFm(track, images, '')
var args = getInfoStub.args
assert(args[0][0].artist === 'artistMock')
assert.equal(args[0][0].artist, 'artistMock')
getInfoStub.restore()
})
@ -141,7 +129,7 @@ describe('CoverArt', function () {
getInfoStub.yieldsTo('success', getInfoResultMock)
coverArt.getCoverFromLastFm(track, images, 'small')
$(images).prop('src').should.endWith('mockAlbumImageUri')
expect($(images).prop('src')).to.endWith('mockAlbumImageUri')
getInfoStub.restore()
})
@ -150,23 +138,19 @@ describe('CoverArt', function () {
var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo')
getInfoStub.yieldsTo('error', 'code', 'message')
var consoleSpy = sinon.spy(console, 'error')
var consoleStub = sinon.stub(console, 'error')
coverArt.getCoverFromLastFm(track, images, '')
assert(consoleSpy.calledOnce)
assert.isTrue(consoleStub.calledOnce)
getInfoStub.restore()
consoleSpy.restore()
consoleStub.restore()
})
})
describe('#getArtistImage()', function () {
beforeEach(function () {
$(images).removeAttr('src')
})
it('should use default image if no artist is provided', function () {
coverArt.getArtistImage('', images, '')
$(images).prop('src').should.endWith('images/user_24x32.png')
expect($(images).prop('src')).to.endWith('images/user_24x32.png')
})
it('should get artist info from last.fm', function () {
@ -176,7 +160,7 @@ describe('CoverArt', function () {
getInfoStub.yieldsTo('success', getInfoResultMock)
coverArt.getArtistImage('mockArtist', images, 'small')
$(images).prop('src').should.endWith('mockArtistImageUri')
expect($(images).prop('src')).to.endWith('mockArtistImageUri')
getInfoStub.restore()
})
@ -184,12 +168,12 @@ describe('CoverArt', function () {
var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo')
getInfoStub.yieldsTo('error', 'code', 'message')
var consoleSpy = sinon.spy(console, 'error')
var consoleStub = sinon.stub(console, 'error')
coverArt.getArtistImage('mockArtist', images, 'small')
assert(consoleSpy.calledOnce)
assert.isTrue(consoleStub.calledOnce)
getInfoStub.restore()
consoleSpy.restore()
consoleStub.restore()
})
})
})

56
tests/js/test_library.js Normal file
View File

@ -0,0 +1,56 @@
var chai = require('chai')
var expect = chai.expect
var assert = chai.assert
chai.use(require('chai-string'))
chai.use(require('chai-jquery'))
var sinon = require('sinon')
var library = require('../../mopidy_musicbox_webclient/static/js/library.js')
describe('Library', function () {
var selectID = '#selectSearchService'
var schemesArray = ['mockScheme1', 'mockScheme2', 'mockScheme3']
var mopidy = { getUriSchemes: function () { return $.when(schemesArray) } }
before(function () {
$(document.body).append('<select id="selectSearchService"></select>')
$('#selectSearchService').selectmenu()
})
describe('#getSearchSchemes()', function () {
beforeEach(function () {
$(selectID).empty()
})
it('should add human-readable options for backend schemes', function () {
uriHumanList = [['mockScheme2', 'mockUriHuman2']]
library.getSearchSchemes([], mopidy)
assert.equal($(selectID).children().length, schemesArray.length + 1)
expect($(selectID).children(':eq(2)')).to.have.text('MockUriHuman2')
})
it('should get default value from cookie', function () {
$.cookie('searchScheme', 'mockScheme3')
library.getSearchSchemes([], mopidy)
expect($(selectID + ' option:selected')).to.have.value('mockScheme3')
})
it('should default to "all" backends if no cookie is available', function () {
$.removeCookie('searchScheme')
library.getSearchSchemes([], mopidy)
expect($(selectID + ' option:selected')).to.have.value('all')
})
it('should capitalize first character of backend schema', function () {
library.getSearchSchemes([], mopidy)
expect($(selectID).children(':eq(1)')).to.have.text('MockScheme1')
})
it('should blacklist services that should not be searched', function () {
library.getSearchSchemes(['mockScheme2'], mopidy)
assert.equal($(selectID).children().length, schemesArray.length)
expect($(selectID).children()).not.to.contain('mockScheme2')
})
})
})

View File

@ -0,0 +1,352 @@
var chai = require('chai')
var expect = chai.expect
var assert = chai.assert
chai.use(require('chai-string'))
chai.use(require('chai-jquery'))
var sinon = require('sinon')
var SyncedProgressTimer = require('../../mopidy_musicbox_webclient/static/js/synced_timer.js')
describe('SyncedTimer', function () {
var MAX_ATTEMPTS = 8
var syncedProgressTimer
var mopidy
var playback
var getTimePositionStub
before(function () {
$(document.body).append(
'<div id="slidercontainer"><!-- slider for track position -->' +
'<span id="songelapsed"></span>' +
'<span id="songlength"></span>' +
'<label for="trackslider" disabled="disabled">Position</label>' +
'<input id="trackslider" name="trackslider"/>' +
'</div>'
)
$('#trackslider').slider() // Initialize slider
})
beforeEach(function () {
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))
}
mopidy = sinon.stub(mopidy)
syncedProgressTimer = new SyncedProgressTimer(MAX_ATTEMPTS, mopidy)
syncedProgressTimer._isConnected = true
})
afterEach(function () {
getTimePositionStub.restore()
})
describe('#SyncedTimer()', function () {
it('should add text nodes to DOM for position and duration indicators', function () {
expect($('#songelapsed')).to.have.text('')
expect($('#songlength')).to.have.text('')
})
it('should start out in unsynced state', function () {
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED)
})
})
describe('#format()', function () {
it('should set value of text node', function () {
assert.equal(SyncedProgressTimer.format(1000), '0:01')
})
it('should handle Infinity', function () {
assert.equal(SyncedProgressTimer.format(Infinity), '(n/a)')
})
it('should handle zero', function () {
assert.equal(SyncedProgressTimer.format(0), '0:00')
})
})
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
})
})
afterEach(function () {
clock.restore()
})
it('should not try to sync unless connected to mopidy', function () {
syncedProgressTimer._isConnected = false
var _syncScheduledStub = sinon.stub(syncedProgressTimer, '_isSyncScheduled')
assert.isFalse(_syncScheduledStub.called, '_syncScheduledStub called')
_syncScheduledStub.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')
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 when scheduled', function () {
var syncStub = sinon.stub(syncedProgressTimer, '_doSync')
syncedProgressTimer.set(0, 1000).start()
clock.tick(250)
assert.isTrue(syncStub.called, '_doSync not called')
syncStub.restore()
})
it('should perform sync', 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
wasSyncing = wasSyncing || syncedProgressTimer.syncState === SyncedProgressTimer.SYNC_STATE.SYNCING
}
assert.isTrue(wasSyncing, 'Timer never entered the "syncing" state')
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCED, 'Timer failed to sync')
})
})
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 () {
syncedProgressTimer._update(1000, Infinity)
assert.equal(syncedProgressTimer.durationNode.nodeValue, '(n/a)')
})
it('should show "(wait)" while waiting for Mopidy to supply a position', function () {
syncedProgressTimer._update(1000, 2000)
assert.equal(syncedProgressTimer.positionNode.nodeValue, '(wait)')
})
it('should show "(sync)" while trying to sync up with Mopidy', function () {
syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING
syncedProgressTimer._update(1000, 2000)
assert.equal(syncedProgressTimer.positionNode.nodeValue, '(sync)')
})
it('should update position text and position track slider when synced', function () {
syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
syncedProgressTimer._update(1000, 2000)
assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:01')
assert.equal($('#trackslider').val(), 1000)
})
})
describe('#_isSyncScheduled()', function () {
var scheduleSpy
var clock
before(function () {
scheduleSpy = sinon.spy(syncedProgressTimer, '_isSyncScheduled')
clock = sinon.useFakeTimers()
})
after(function () {
scheduleSpy.restore()
clock.restore()
})
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())
})
})
describe('#_doSync', function () {
var clock
beforeEach(function () {
clock = sinon.useFakeTimers()
})
afterEach(function () {
clock.restore()
})
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')
})
it('should request position from Mopidy', function () {
syncedProgressTimer._doSync(1000, 2000)
assert.isTrue(getTimePositionStub.called, 'getTimePosition not called')
})
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)
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
clock.tick(10)
syncedProgressTimer._doSync(1020, 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)
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
assert.equal(syncedProgressTimer._syncAttemptsRemaining, syncedProgressTimer._maxAttempts)
})
it('should step back exponentially while syncing', function () {
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')
}
})
it('should check sync every 32s once synced', function () {
syncedProgressTimer._syncAttemptsRemaining = 0
syncedProgressTimer._doSync(1000, 2000)
assert.equal(syncedProgressTimer._scheduledSyncTime, 32000)
})
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)
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
clock.tick(10)
syncedProgressTimer._doSync(1010, 2000)
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
})
})
describe('#set()', function () {
it('should throw exception if no arguments are provided', function () {
assert.throw(function () { syncedProgressTimer.set() }, Error)
})
it('should set position if only one argument is provided', function () {
syncedProgressTimer.set(1000)
assert.equal(syncedProgressTimer._progressTimer._state.position, 1000)
})
it('should update track slider if no sync is scheduled', function () {
syncedProgressTimer.stop()
syncedProgressTimer.set(1000, 2000)
expect($('#songelapsed').text()).to.endWith('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)
})
})
describe('#start()', function () {
it('should start timer', function () {
var startStub = sinon.stub(syncedProgressTimer._progressTimer, 'start')
syncedProgressTimer.start()
assert(startStub.called)
startStub.restore()
})
it('should always start in unsynced state', function () {
syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
syncedProgressTimer.start()
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED)
})
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())
syncedProgressTimer.start()
clock.tick(1000)
expect(syncedProgressTimer._scheduledSyncTime).to.be.below(new Date().getTime())
clock.restore()
})
})
describe('#stop()', function () {
it('should stop timer', function () {
var stopStub = sinon.stub(syncedProgressTimer._progressTimer, 'stop')
syncedProgressTimer.stop()
assert(stopStub.called)
stopStub.restore()
})
it('should show position when stopped', function () {
syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
syncedProgressTimer._update(1000, 5000)
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')
})
it('should cancel any scheduled syncs', function () {
syncedProgressTimer._scheduledSyncTime = 5000
syncedProgressTimer.stop()
expect(syncedProgressTimer._scheduledSyncTime).to.be.null
})
})
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')
var stopStub = sinon.stub(syncedProgressTimer, 'stop')
syncedProgressTimer.reset()
assert(resetStub.called)
assert(initStub.called)
assert(stopStub.called)
assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:00')
assert.equal(syncedProgressTimer.durationNode.nodeValue, '(n/a)')
resetStub.restore()
initStub.restore()
stopStub.restore()
})
})
describe('#updatePosition()', function () {
it('should format and set position node', function () {
var formatSpy = sinon.spy(SyncedProgressTimer, 'format')
assert.equal(syncedProgressTimer.positionNode.nodeValue, '')
syncedProgressTimer.updatePosition(1000)
assert.isTrue(formatSpy.called)
expect(syncedProgressTimer.positionNode.nodeValue).to.endWith('0:01')
formatSpy.restore()
})
})
})

View File

@ -1,54 +0,0 @@
var chai = require('chai')
var should = chai.should()
var expect = chai.expect
var assert = chai.assert
chai.use(require('chai-string'))
chai.use(require('chai-jquery'))
var sinon = require('sinon')
var library = require('../mopidy_musicbox_webclient/static/js/library.js')
var selectID = '#selectSearchService'
var schemesArray
before(function () {
$(document.body).append('<select id="selectSearchService"></select>')
$('#selectSearchService').selectmenu()
})
describe('Library', function () {
describe('#getSearchSchemes()', function () {
beforeEach(function () {
schemesArray = ['mockScheme1', 'mockScheme2', 'mockScheme3']
mopidy = {
getUriSchemes: function () { return $.when(schemesArray) }
}
$(selectID).empty()
})
it('should add human-readable options for backend schemes', function () {
uriHumanList = [['mockScheme2', 'mockUriHuman2']]
library.getSearchSchemes()
assert($(selectID).children().length === schemesArray.length + 1)
$(selectID).children(':eq(2)').should.have.text('MockUriHuman2')
})
it('should get default value from cookie', function () {
$.cookie('searchScheme', 'mockScheme3')
library.getSearchSchemes()
$(selectID + ' option:selected').should.have.value('mockScheme3')
})
it('should default to "all" backends if no cookie is available', function () {
$.removeCookie('searchScheme')
library.getSearchSchemes()
$(selectID + ' option:selected').should.have.value('all')
})
it('should capitalize first character of backend schema', function () {
library.getSearchSchemes()
$(selectID).children(':eq(1)').should.have.text('MockScheme1')
})
})
})