Add Javascript test and test coverage frameworks.

This commit is contained in:
jcass 2016-03-10 17:21:13 +02:00
parent df0775e330
commit f8db8be71f
10 changed files with 421 additions and 86 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@
MANIFEST MANIFEST
build/ build/
cover/ cover/
coverage/
coverage.xml coverage.xml
dist/ dist/
docs/_build/ docs/_build/

View File

@ -8,6 +8,7 @@ python:
env: env:
- TOX_ENV=py27 - TOX_ENV=py27
- TOX_ENV=flake8 - TOX_ENV=flake8
- TOX_ENV=test
- TOX_ENV=eslint - TOX_ENV=eslint
- TOX_ENV=csslint - TOX_ENV=csslint
- TOX_ENV=tidy - TOX_ENV=tidy
@ -19,4 +20,6 @@ script:
- "tox -e $TOX_ENV" - "tox -e $TOX_ENV"
after_success: after_success:
- "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi" # TODO: find a way to combine .py and .js coverage reports.
# - "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi"
- "if [ $TOX_ENV == 'test' ]; then cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js; fi"

81
karma.conf.js Normal file
View File

@ -0,0 +1,81 @@
// Karma configuration
module.exports = function (config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['browserify', 'mocha'],
// 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'
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'tests/**/test_*.js': [ 'browserify' ],
'mopidy_musicbox_webclient/static/js/**/*.js': ['coverage']
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'coverage'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
// add additional browserify configuration properties here
// such as transform and/or debug=true to generate source maps
browserify: {
debug: true,
transform: [
'babelify',
['browserify-istanbul', { instrumenter: require('isparta') }]
]
},
coverageReporter: {
// specify a common output directory
dir: 'coverage/',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text'}
]
}
})
}

View File

@ -128,7 +128,7 @@ function setSongInfo (data) {
} }
if (data.track.album && data.track.album.name) { if (data.track.album && data.track.album.name) {
$('#modalalbum').html('<a href="#" onclick="return showAlbum(\'' + data.track.album.uri + '\');">' + data.track.album.name + '</a>') $('#modalalbum').html('<a href="#" onclick="return showAlbum(\'' + data.track.album.uri + '\');">' + data.track.album.name + '</a>')
getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge') coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge')
} else { } else {
$('#modalalbum').html('') $('#modalalbum').html('')
$('#infocover').attr('src', 'images/default_cover.png') $('#infocover').attr('src', 'images/default_cover.png')

View File

@ -5,27 +5,20 @@
API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733' API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733'
API_SECRET = '2c631802c2285d5d5d1502462fe42a2b' API_SECRET = '2c631802c2285d5d5d1502462fe42a2b'
var fmcache var coverArt = {
var lastfm fmcache: new LastFMCache(),
lastfm: new LastFM({
$(window).load(function () {
// create a Cache object
fmcache = new LastFMCache()
// create a LastFM object
lastfm = new LastFM({
apiKey: API_KEY, apiKey: API_KEY,
apiSecret: API_SECRET, apiSecret: API_SECRET,
cache: fmcache cache: this.fmcache
}) }),
})
function getCover (uri, images, size) { getCover: function (uri, images, size) {
var defUrl = 'images/default_cover.png' var defUrl = 'images/default_cover.png'
$(images).attr('src', defUrl) $(images).attr('src', defUrl)
if (!uri) { if (!uri) {
return return
} }
mopidy.library.getImages({'uris': [uri]}).then(function (imageResults) { mopidy.library.getImages({'uris': [uri]}).then(function (imageResults) {
var uri = Object.keys(imageResults)[0] var uri = Object.keys(imageResults)[0]
if (imageResults[uri].length > 0) { if (imageResults[uri].length > 0) {
@ -33,35 +26,43 @@ function getCover (uri, images, size) {
} else { } else {
// Also check deprecated 'album.images' in case backend does not // Also check deprecated 'album.images' in case backend does not
// implement mopidy.library.getImages yet... // implement mopidy.library.getImages yet...
getCoverFromAlbum(uri, images, size) coverArt.getCoverFromAlbum(uri, images, size)
} }
}) })
} },
// Note that this approach has been deprecated in Mopidy // Note that this approach has been deprecated in Mopidy
// TODO: Remove when Mopidy no longer supports getting images // TODO: Remove when Mopidy no longer supports getting images
// with 'album.images'. // with 'album.images'.
function getCoverFromAlbum (uri, images, size) { getCoverFromAlbum: function (uri, images, size) {
var defUrl = 'images/default_cover.png'
$(images).attr('src', defUrl)
if (!uri) {
return
}
mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) { mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
var uri = Object.keys(resultDict)[0] var uri = Object.keys(resultDict)[0]
var track = resultDict[uri][0] var track = resultDict[uri][0]
if (track.album && track.album.images && (track.album.images.length > 0)) { if (track && track.album && track.album.images && track.album.images.length > 0) {
$(images).attr('src', track.album.images[0]) $(images).attr('src', track.album.images[0])
} else { } else if (track && (track.album || track.artist)) {
// Fallback to last.fm // Fallback to last.fm
getCoverFromLastFm(track, images, size) coverArt.getCoverFromLastFm(track, images, size)
} } else {
})
}
function getCoverFromLastFm (track, images, size) {
var defUrl = 'images/default_cover.png'
if (!(track.album || track.artist)) {
return return
} }
var albumname = track.album.name || '' })
},
getCoverFromLastFm: function (track, images, size) {
var defUrl = 'images/default_cover.png'
$(images).attr('src', defUrl)
if (!track || !(track.album || track.artists)) {
return
}
var albumname = (track.album && track.album.name) ? track.album.name : ''
var artistname = '' var artistname = ''
if (track.album.artists && (track.album.artists.length > 0)) { if (track.album && track.album.artists && track.album.artists.length > 0) {
// First look for the artist in the album // First look for the artist in the album
artistname = track.album.artists[0].name artistname = track.album.artists[0].name
} else if (track.artists && (track.artists.length > 0)) { } else if (track.artists && (track.artists.length > 0)) {
@ -69,24 +70,41 @@ function getCoverFromLastFm (track, images, size) {
artistname = track.artists[0].name artistname = track.artists[0].name
} }
lastfm.album.getInfo({artist: artistname, album: albumname}, this.lastfm.album.getInfo({artist: artistname, album: albumname}, {success: function (data) {
{ success: function (data) {
for (var i = 0; i < data.album.image.length; i++) { for (var i = 0; i < data.album.image.length; i++) {
if (data.album.image[i].size === size) { if (data.album.image[i].size === size) {
$(images).attr('src', data.album.image[i]['#text'] || defUrl) $(images).attr('src', data.album.image[i]['#text'] || defUrl)
} }
} }
} }, error: function (code, message) {
}, $(images).attr('src', defUrl)) console.log('Error retrieving album info from last.fm', code, message)
} }})
},
function getArtistImage (nwartist, image, size) { getArtistImage: function (artist, images, size) {
var defUrl = 'images/user_24x32.png' var defUrl = 'images/user_24x32.png'
lastfm.artist.getInfo({artist: nwartist}, {success: function (data) { $(images).attr('src', defUrl)
if (!artist || artist.length === 0) {
return
}
this.lastfm.artist.getInfo({artist: artist}, {success: function (data) {
for (var i = 0; i < data.artist.image.length; i++) { for (var i = 0; i < data.artist.image.length; i++) {
if (data.artist.image[i].size === size) { if (data.artist.image[i].size === size) {
$(image).attr('src', data.artist.image[i]['#text'] || defUrl) $(images).attr('src', data.artist.image[i]['#text'] || defUrl)
} }
} }
}}, $(images).attr('src', defUrl)) }, error: function (code, message) {
console.log('Error retrieving artist info from last.fm', code, message)
}})
}
}
$(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
}
} }

View File

@ -220,7 +220,7 @@ function processCurrentPlaylist (resultArr) {
function processArtistResults (resultArr) { function processArtistResults (resultArr) {
if (!resultArr || (resultArr.length === 0)) { if (!resultArr || (resultArr.length === 0)) {
$('#h_artistname').text('Artist not found...') $('#h_artistname').text('Artist not found...')
getCover('', '#artistviewimage, #artistpopupimage', 'extralarge') coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge')
showLoading(false) showLoading(false)
return return
} }
@ -239,7 +239,7 @@ function processArtistResults (resultArr) {
function processAlbumResults (resultArr) { function processAlbumResults (resultArr) {
if (!resultArr || (resultArr.length === 0)) { if (!resultArr || (resultArr.length === 0)) {
$('#h_albumname').text('Album not found...') $('#h_albumname').text('Album not found...')
getCover('', '#albumviewcover, #coverpopupimage', 'extralarge') coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge')
showLoading(false) showLoading(false)
return return
} }
@ -252,6 +252,6 @@ function processAlbumResults (resultArr) {
$('#h_albumartist').html(artistname) $('#h_albumartist').html(artistname)
$('#coverpopupalbumname').html(albumname) $('#coverpopupalbumname').html(albumname)
$('#coverpopupartist').html(artistname) $('#coverpopupartist').html(artistname)
getCover(resultArr[0].uri, '#albumviewcover, #coverpopupimage', 'extralarge') coverArt.getCover(resultArr[0].uri, '#albumviewcover, #coverpopupimage', 'extralarge')
showLoading(false) showLoading(false)
} }

View File

@ -1,5 +1,4 @@
var progressTimer var progressTimer
var progressElement = document.getElementById('trackslider')
var positionNode = document.createTextNode('') var positionNode = document.createTextNode('')
var durationNode = document.createTextNode('') var durationNode = document.createTextNode('')
@ -13,8 +12,10 @@ var syncsLeft = MAX_SYNCS
var synced = false var synced = false
var consecutiveSyncs = 0 var consecutiveSyncs = 0
document.getElementById('songelapsed').appendChild(positionNode) $(document).ready(function () {
document.getElementById('songlength').appendChild(durationNode) $('#songelapsed').append(positionNode)
$('#songlength').append(durationNode)
})
function timerCallback (position, duration, isRunning) { function timerCallback (position, duration, isRunning) {
updateTimers(position, duration, isRunning) updateTimers(position, duration, isRunning)

View File

@ -7,8 +7,8 @@
"test": "tests" "test": "tests"
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "karma start karma.conf.js",
"eslint": "eslint mopidy_musicbox_webclient/static/js/**.js", "eslint": "eslint mopidy_musicbox_webclient/static/js/**/**.js tests/**/test_*.js",
"csslint": "csslint mopidy_musicbox_webclient/static/css/**.css", "csslint": "csslint mopidy_musicbox_webclient/static/css/**.css",
"tidy": "node tidy.js" "tidy": "node tidy.js"
}, },
@ -22,12 +22,32 @@
"url": "https://github.com/pimusicbox/mopidy-musicbox-webclient/issues" "url": "https://github.com/pimusicbox/mopidy-musicbox-webclient/issues"
}, },
"devDependencies": { "devDependencies": {
"eslint": "latest", "babelify": "^7.2.0",
"eslint-config-standard": "latest", "browserify": "^13.0.0",
"eslint-plugin-standard": "latest", "browserify-istanbul": "^2.0.0",
"eslint-plugin-promise": "latest", "chai": "^3.5.0",
"csslint": "latest", "chai-as-promised": "^5.2.0",
"tidy-html5": "latest" "chai-jquery": "^2.0.0",
"chai-string": "^1.2.0",
"coveralls": "^2.11.8",
"csslint": "^0.10.0",
"eslint": "^2.3.0",
"eslint-config-standard": "^5.1.0",
"eslint-plugin-promise": "^1.1.0",
"eslint-plugin-standard": "^1.3.2",
"install": "^0.5.6",
"isparta": "^4.0.0",
"karma": "^0.13.22",
"karma-browserify": "^5.0.2",
"karma-cli": "^0.1.2",
"karma-coverage": "^0.5.5",
"karma-mocha": "^0.2.2",
"karma-phantomjs-launcher": "^1.0.0",
"mocha": "^2.4.5",
"phantomjs-prebuilt": "^2.1.5",
"sinon": "^1.17.3",
"tidy-html5": "latest",
"watchify": "^3.7.0"
}, },
"homepage": "https://github.com/pimusicbox/mopidy-musicbox-webclient#readme" "homepage": "https://github.com/pimusicbox/mopidy-musicbox-webclient#readme"
} }

201
tests/test_images.js Normal file
View File

@ -0,0 +1,201 @@
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 coverArt = require('../mopidy_musicbox_webclient/static/js/images.js')
var images
before(function () {
html =
'<span id="songelapsed" class="pull-left"></span>' +
'<span id="songlength" class="pull-right"></span>'
$(document).ready(function () {
$(document.body).add(html)
})
mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'}))
images = $('<img id="img_mock">')
})
describe('CoverArt', function () {
describe('#getCover()', function () {
beforeEach(function () {
$(images).removeAttr('src')
})
it('should use default image if no track URI is provided', function () {
coverArt.getCover('', images, '')
$(images).prop('src').should.endWith('images/default_cover.png')
})
it('should get image from Mopidy, if available', function () {
var getImagesResultMock = {'mock:track:uri': [{uri: 'mockImageUri'}]}
var library = { getImages: function () { return $.when(getImagesResultMock) } }
mopidy.library = library
var getImagesSpy = sinon.spy(mopidy.library, 'getImages')
coverArt.getCover('mock:track:uri', images, '')
assert(getImagesSpy.calledOnce)
$(images).prop('src').should.endWith('mockImageUri')
})
it('should fall back to retrieving image from deprecated track.album.images', function () {
var getImagesResultMock = {'mock:track:uri': []}
var lookupResultMock = {'mock:track:uri': [{album: {images: ['mockAlbumImageUri']}}]}
var library = {
getImages: function () { return $.when(getImagesResultMock) },
lookup: function () { return $.when(lookupResultMock) }
}
mopidy.library = library
var getImagesSpy = sinon.spy(mopidy.library, 'getImages')
var getCoverFromAlbumSpy = sinon.spy(coverArt, 'getCoverFromAlbum')
coverArt.getCover('mock:track:uri', images, '')
assert(getImagesSpy.calledOnce)
assert(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')
})
it('should get image from Mopidy track.album.images, if available', function () {
var lookupResultMock = {'mock:track:uri': [{album: {images: ['mockAlbumImageUri']}}]}
var library = {
lookup: function () { return $.when(lookupResultMock) }
}
mopidy.library = library
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
coverArt.getCoverFromAlbum('mock:track:uri', images, '')
assert(lookupSpy.calledOnce)
$(images).prop('src').should.endWith('mockAlbumImageUri')
})
it('should use default image if track.album or track.artist is not available', function () {
var lookupResultMock = {'mock:track:uri': []}
var library = {
lookup: function () { return $.when(lookupResultMock) }
}
mopidy.library = library
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
coverArt.getCoverFromAlbum('mock:track:uri', images, '')
assert(lookupSpy.calledOnce)
$(images).prop('src').should.endWith('images/default_cover.png')
})
it('should fall back to retrieving image from last.fm if none provided by Mopidy', function () {
var lookupResultMock = {'mock:track:uri': [{album: {images: []}}]}
var library = {
lookup: function () { return $.when(lookupResultMock) }
}
mopidy.library = library
var getCoverFromLastFmSpy = sinon.spy(coverArt, 'getCoverFromLastFm')
coverArt.getCoverFromAlbum('mock:track:uri', images, '')
assert(getCoverFromLastFmSpy.calledOnce)
})
})
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')
})
it('should fall back to using track artist if album artist is not available', function () {
var track = {artists: [{name: 'artistMock'}]}
var getInfoResultMock = {album: {image: []}}
var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo')
getInfoStub.returns($.when(getInfoResultMock))
coverArt.getCoverFromLastFm(track, images, '')
var args = getInfoStub.args
assert(args[0][0].artist === 'artistMock')
getInfoStub.restore()
})
it('should get album info from last.fm', function () {
var track = {album: {artists: [{name: 'albumMock'}]}}
var getInfoResultMock = {album: {image: [{'#text': 'mockAlbumImageUri', size: 'small'}]}}
var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo')
getInfoStub.yieldsTo('success', getInfoResultMock)
coverArt.getCoverFromLastFm(track, images, 'small')
$(images).prop('src').should.endWith('mockAlbumImageUri')
getInfoStub.restore()
})
it('should log errors', function () {
var track = {album: {artists: [{name: 'albumMock'}]}}
var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo')
getInfoStub.yieldsTo('error', 'code', 'message')
var consoleSpy = sinon.spy(console, 'log')
coverArt.getCoverFromLastFm(track, images, '')
assert(consoleSpy.calledOnce)
getInfoStub.restore()
consoleSpy.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')
})
it('should get artist info from last.fm', function () {
var getInfoResultMock = {artist: {image: [{'#text': 'mockArtistImageUri', size: 'small'}]}}
var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo')
getInfoStub.yieldsTo('success', getInfoResultMock)
coverArt.getArtistImage('mockArtist', images, 'small')
$(images).prop('src').should.endWith('mockArtistImageUri')
getInfoStub.restore()
})
it('should log errors', function () {
var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo')
getInfoStub.yieldsTo('error', 'code', 'message')
var consoleSpy = sinon.spy(console, 'log')
coverArt.getArtistImage('mockArtist', images, 'small')
assert(consoleSpy.calledOnce)
getInfoStub.restore()
consoleSpy.restore()
})
})
})

14
tox.ini
View File

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py27, flake8, eslint, csslint, tidy envlist = py27, flake8, test, eslint, csslint, tidy
[testenv] [testenv]
deps = deps =
@ -20,7 +20,17 @@ deps =
flake8 flake8
flake8-import-order flake8-import-order
skip_install = true skip_install = true
commands = flake8 {posargs:mopidy_musicbox_webclient} commands = flake8 {posargs:mopidy_musicbox_webclient tests}
[testenv:test]
whitelist_externals =
/bin/bash
deps =
nodeenv
skip_install = true
commands =
- nodeenv --prebuilt {toxworkdir}/node_env
bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm test'
[testenv:eslint] [testenv:eslint]
whitelist_externals = whitelist_externals =