Combine .js and .py coverage reports.

Remove services that cannot be searched from search dropdown.

Refactoring and additional unit tests.
This commit is contained in:
jcass 2016-03-28 05:46:26 +02:00
parent 2629464c18
commit 411a3781fd
18 changed files with 312 additions and 79 deletions

2
.gitignore vendored
View File

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

View File

@ -1,10 +1,18 @@
sudo: false sudo: required
dist: trusty
language: python language: python
python: python:
- "2.7_with_system_site_packages" - "2.7_with_system_site_packages"
addons:
apt:
sources:
- mopidy-stable
packages:
- mopidy
env: env:
- TOX_ENV=py27 - TOX_ENV=py27
- TOX_ENV=flake8 - TOX_ENV=flake8
@ -13,6 +21,11 @@ env:
- TOX_ENV=csslint - TOX_ENV=csslint
- TOX_ENV=tidy - TOX_ENV=tidy
before_install:
- "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround https://github.com/tornadoweb/tornado/issues/1573
- "sudo apt-get update -qq"
- "sudo apt-get install -y gstreamer0.10-plugins-good python-gst0.10"
install: install:
- "pip install tox" - "pip install tox"
@ -20,6 +33,4 @@ script:
- "tox -e $TOX_ENV" - "tox -e $TOX_ENV"
after_success: after_success:
# TODO: find a way to combine .py and .js coverage reports. - "if [ $TOX_ENV == 'test' ]; then pip install coveralls; coveralls --merge=./karma_coverage/coverage-final.json; fi"
# - "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"

View File

@ -71,9 +71,10 @@ module.exports = function (config) {
coverageReporter: { coverageReporter: {
// specify a common output directory // specify a common output directory
dir: 'coverage/', dir: '.karma_coverage/',
reporters: [ reporters: [
{ type: 'lcov', subdir: '.' }, { type: 'lcov', subdir: '.' },
{ type: 'json', subdir: '.' },
{ type: 'text' } { type: 'text' }
] ]
} }

View File

@ -7,7 +7,7 @@ from mopidy import config, ext
__version__ = '2.2.0' __version__ = '2.2.0'
class MusicBoxExtension(ext.Extension): class Extension(ext.Extension):
dist_name = 'Mopidy-MusicBox-Webclient' dist_name = 'Mopidy-MusicBox-Webclient'
ext_name = 'musicbox_webclient' ext_name = 'musicbox_webclient'
@ -18,7 +18,7 @@ class MusicBoxExtension(ext.Extension):
return config.read(conf_file) return config.read(conf_file)
def get_config_schema(self): def get_config_schema(self):
schema = super(MusicBoxExtension, self).get_config_schema() schema = super(Extension, self).get_config_schema()
schema['musicbox'] = config.Boolean(optional=True) schema['musicbox'] = config.Boolean(optional=True)
schema['websocket_host'] = config.Hostname(optional=True) schema['websocket_host'] = config.Hostname(optional=True)
schema['websocket_port'] = config.Port(optional=True) schema['websocket_port'] = config.Port(optional=True)

View File

@ -1,30 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html manifest="mb.appcache"> <html manifest="mb.appcache">
<head> <head>
<title>Musicbox</title> <title>{{ title }}</title>
<meta charset="utf-8"> <meta charset="utf-8">
<script type="text/javascript" src="vendors/jquery/jquery-1.12.0.min.js"></script>
<link rel="stylesheet" type="text/css" href="vendors/jquery_mobile_flat_ui_theme/jquery.mobile.flatui.min.css"/>
<script>
//configuration
var isMusicBox = '{{musicbox}}' == 'True'; // Remove MusicBox only content (e.g. settings, system pages)
var websocketUrl = ('{{useWebsocketUrl}}' == 'True') ? '{{websocket_url}}' : ''
var hasAlarmClock = '{{alarmclock}}' == 'True'; // Add Alarm Clock icons
$(document).bind("mobileinit", function () { <script type="text/javascript" src="vendors/jquery/jquery-1.12.0.min.js"></script>
$.extend($.mobile, { <script type="text/javascript" src="vendors/jquery_cookie/jquery.cookie.js"></script>
ajaxEnabled: false, <script type="text/javascript" src="js/custom_scripting.js"></script>
hashListeningEnabled: false
// linkBindingEnabled: false <link rel="stylesheet" type="text/css" href="vendors/jquery_mobile_flat_ui_theme/jquery.mobile.flatui.min.css"/>
// buttonMarkup.hoverDelay: 100,
// buttonMarkup.corners: false
});
});
/* window.addEventListener('load', function () {
new FastClick(document.body);
}, false);
*/
</script>
<link rel="icon" type="image/gif" href="images/icons/musicbox32.gif" /> <link rel="icon" type="image/gif" href="images/icons/musicbox32.gif" />
<link rel="apple-touch-icon" href="images/icons/musicbox57.png" /> <link rel="apple-touch-icon" href="images/icons/musicbox57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="images/icons/musicbox72.png" /> <link rel="apple-touch-icon" sizes="72x72" href="images/icons/musicbox72.png" />
@ -52,7 +36,7 @@
</head> </head>
<body> <body data-websocket-url="{{websocketUrl}}" data-is-musicbox="{{isMusicBox}}" data-has-alarmclock="{{hasAlarmClock}}">
<div data-role="page" id="page" class="ui-responsive-panel" data-theme="c"> <div data-role="page" id="page" class="ui-responsive-panel" data-theme="c">
<div data-role="panel" id="panel" data-position="left" data-theme="a" data-display="reveal" data-position-fixed="true"> <div data-role="panel" id="panel" data-position="left" data-theme="a" data-display="reveal" data-position-fixed="true">
@ -501,7 +485,6 @@
</div> </div>
<!-- /page one --> <!-- /page one -->
<script type="text/javascript" src="vendors/mopidy/mopidy.min.js"></script> <script type="text/javascript" src="vendors/mopidy/mopidy.min.js"></script>
<script type="text/javascript" src="vendors/jquery_cookie/jquery.cookie.js"></script>
<script type="text/javascript" src="vendors/media_progress_timer/timer.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/progress_timer.js"></script>
<script type="text/javascript" src="js/controls.js"></script> <script type="text/javascript" src="js/controls.js"></script>

View File

@ -0,0 +1,8 @@
// jQuery Mobile configuration options
// see: http://api.jquerymobile.com/1.3/global-config/
$(document).bind('mobileinit', function () {
$.extend($.mobile, {
ajaxEnabled: false,
hashListeningEnabled: false
})
})

View File

@ -110,6 +110,19 @@ var uriHumanList = [
['subsonic', 'Subsonic'] ['subsonic', 'Subsonic']
] ]
// List of Mopidy URI schemes that should not be searched directly.
// Also blacklists 'yt' in favour of using the other 'youtube' supported scheme.
var searchBlacklist = [
'file',
'http',
'https',
'mms',
'rtmp',
'rtmps',
'rtsp',
'yt'
]
function scrollToTop () { function scrollToTop () {
var divtop = 0 var divtop = 0
$('body,html').animate({ $('body,html').animate({

View File

@ -475,6 +475,7 @@ $(document).ready(function (event) {
$(window).hashchange() $(window).hashchange()
// Connect to server // Connect to server
var websocketUrl = $(document.body).data('websocket-url')
if (websocketUrl) { if (websocketUrl) {
try { try {
mopidy = new Mopidy({ mopidy = new Mopidy({
@ -539,16 +540,16 @@ $(document).ready(function (event) {
} }
}) })
// remove buttons only for MusicBox // Remove MusicBox only content (e.g. settings, system pages)
if (!isMusicBox) { if (!$(document.body).data('is-musicbox')) {
$('#navSettings').hide() $('#navSettings').hide()
$('#navshutdown').hide() $('#navshutdown').hide()
$('#homesettings').hide() $('#homesettings').hide()
$('#homeshutdown').hide() $('#homeshutdown').hide()
} }
// remove Alarm Clock if it is not present // Remove Alarm Clock icons if it is not present
if (!hasAlarmClock) { if (!$(document.body).data('has-alarmclock')) {
$('#navAlarmClock').hide() $('#navAlarmClock').hide()
$('#homeAlarmClock').hide() $('#homeAlarmClock').hide()
$('#homeAlarmClock').nextAll().find('.ui-block-a, .ui-block-b').toggleClass('ui-block-a').toggleClass('ui-block-b') $('#homeAlarmClock').nextAll().find('.ui-block-a, .ui-block-b').toggleClass('ui-block-a').toggleClass('ui-block-b')

View File

@ -322,8 +322,11 @@ var library = {
searchScheme = 'all' searchScheme = 'all'
} }
$('#selectSearchService').empty() $('#selectSearchService').empty()
$('#selectSearchService').append(new Option('All backends', 'all')) $('#selectSearchService').append(new Option('All services', 'all'))
mopidy.getUriSchemes().then(function (schemesArray) { mopidy.getUriSchemes().then(function (schemesArray) {
schemesArray = schemesArray.filter(function (el) {
return searchBlacklist.indexOf(el) < 0
})
for (var i = 0; i < schemesArray.length; i++) { for (var i = 0; i < schemesArray.length; i++) {
backendName = getMediaHuman(schemesArray[i]) backendName = getMediaHuman(schemesArray[i])
backendName = backendName.charAt(0).toUpperCase() + backendName.slice(1) backendName = backendName.charAt(0).toUpperCase() + backendName.slice(1)

View File

@ -1,6 +1,6 @@
CACHE MANIFEST CACHE MANIFEST
# 2016-03-19:v1 # 2016-03-28:v1
NETWORK: NETWORK:
* *
@ -19,8 +19,8 @@ images/icons/play_alt_12x12.png
images/icons/play_alt_16x16.png images/icons/play_alt_16x16.png
images/loader.gif images/loader.gif
images/user_24x32.png images/user_24x32.png
index.html
js/controls.js js/controls.js
js/custom_scripting.js
js/functionsvars.js js/functionsvars.js
js/gui.js js/gui.js
js/images.js js/images.js

View File

@ -1,10 +1,13 @@
from __future__ import unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
import json
import logging import logging
import string
import tornado.web import tornado.web
from . import MusicBoxExtension import mopidy_musicbox_webclient.webclient as mmw
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,40 +24,31 @@ class StaticHandler(tornado.web.StaticFileHandler):
@classmethod @classmethod
def get_version(cls, settings, path): def get_version(cls, settings, path):
return MusicBoxExtension.version return mmw.Extension.version
class IndexHandler(tornado.web.RequestHandler): class IndexHandler(tornado.web.RequestHandler):
def initialize(self, config, path): def initialize(self, config, path):
ext_config = config[MusicBoxExtension.ext_name]
host, port = ext_config['websocket_host'], ext_config['websocket_port'] webclient = mmw.Webclient(config)
ws_url = ''
if host or port:
if not host:
host = self.request.host.partition(':')[0]
logger.warning('Musicbox websocket_host not specified, '
'using %s', host)
elif not port:
port = config['http']['port']
logger.warning('Musicbox websocket_port not specified, '
'using %s', port)
protocol = 'ws'
if self.request.protocol == 'https':
protocol = 'wss'
ws_url = "%s://%s:%d/mopidy/ws" % (protocol, host, port)
self.__dict = { self.__dict = {
'version': MusicBoxExtension.version, 'isMusicBox': json.dumps(webclient.is_music_box()),
'musicbox': ext_config.get('musicbox', False), 'websocketUrl': webclient.get_websocket_url(self.request),
'useWebsocketUrl': ws_url != '', 'hasAlarmClock': json.dumps(webclient.has_alarm_clock()),
'websocket_url': ws_url,
'alarmclock': config.get('alarmclock', {}).get('enabled', False),
} }
self.__path = path self.__path = path
self.__title = string.Template('MusicBox on $hostname')
def get(self, path): def get(self, path):
return self.render('index.html', **self.__dict) return self.render(path, title=self.get_title(), **self.__dict)
def get_title(self):
hostname, sep, port = self.request.host.rpartition(':')
if not sep or not port.isdigit():
hostname, port = self.request.host, '80'
return self.__title.safe_substitute(hostname=hostname, port=port)
def get_template_path(self): def get_template_path(self):
return self.__path return self.__path

View File

@ -0,0 +1,46 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import logging
from mopidy_musicbox_webclient import Extension
logger = logging.getLogger(__name__)
class Webclient(object):
def __init__(self, config):
self.config = config
@property
def ext_config(self):
return self.config.get(Extension.ext_name, {})
@classmethod
def get_version(cls):
return Extension.version
def get_websocket_url(self, request):
host, port = self.ext_config['websocket_host'], self.ext_config['websocket_port']
ws_url = ''
if host or port:
if not host:
host = request.host.partition(':')[0]
logger.warning('Musicbox websocket_host not specified, '
'using %s', host)
elif not port:
port = self.config['http']['port']
logger.warning('Musicbox websocket_port not specified, '
'using %s', port)
protocol = 'ws'
if request.protocol == 'https':
protocol = 'wss'
ws_url = "%s://%s:%d/mopidy/ws" % (protocol, host, port)
return ws_url
def has_alarm_clock(self):
return self.ext_config.get('alarmclock', {}).get('enabled', False)
def is_music_box(self):
return self.ext_config.get('musicbox', False)

View File

@ -29,7 +29,7 @@ setup(
], ],
entry_points={ entry_points={
'mopidy.ext': [ 'mopidy.ext': [
'musicbox_webclient = mopidy_musicbox_webclient:MusicBoxExtension', 'musicbox_webclient = mopidy_musicbox_webclient:Extension',
], ],
}, },
classifiers=[ classifiers=[

View File

@ -1,22 +1,37 @@
from __future__ import unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from mopidy_musicbox_webclient import MusicBoxExtension import unittest
import mock
from mopidy_musicbox_webclient import Extension
def test_get_default_config(): class ExtensionTests(unittest.TestCase):
ext = MusicBoxExtension()
config = ext.get_default_config() def test_get_default_config(self):
ext = Extension()
assert '[musicbox_webclient]' in config config = ext.get_default_config()
assert 'enabled = true' in config
assert '[musicbox_webclient]' in config
assert 'enabled = true' in config
assert 'websocket_host =' in config
assert 'websocket_port =' in config
def test_get_config_schema(): def test_get_config_schema(self):
ext = MusicBoxExtension() ext = Extension()
schema = ext.get_config_schema() schema = ext.get_config_schema()
assert 'musicbox' in schema
assert 'musicbox' in schema
assert 'websocket_host' in schema
assert 'websocket_port' in schema
# TODO Write more tests def test_setup(self):
registry = mock.Mock()
ext = Extension()
ext.setup(registry)
calls = [mock.call('http:app', {'name': ext.ext_name, 'factory': ext.factory})]
registry.add.assert_has_calls(calls, any_order=True)

View File

@ -7,7 +7,7 @@ chai.use(require('chai-jquery'))
var sinon = require('sinon') var sinon = require('sinon')
var coverArt = require('../mopidy_musicbox_webclient/static/js/library.js') var library = require('../mopidy_musicbox_webclient/static/js/library.js')
var selectID = '#selectSearchService' var selectID = '#selectSearchService'
var schemesArray var schemesArray

67
tests/test_web.py Normal file
View File

@ -0,0 +1,67 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import mock
import mopidy.config as config
import tornado.testing
import tornado.web
import tornado.websocket
from mopidy_musicbox_webclient import Extension
from mopidy_musicbox_webclient.web import StaticHandler
class BaseTest(tornado.testing.AsyncHTTPTestCase):
def get_app(self):
extension = Extension()
self.config = config.Proxy({'musicbox_webclient': {
'enabled': True,
'musicbox': True,
'websocket_host': '',
'websocket_port': '',
}
})
return tornado.web.Application(extension.factory(self.config, mock.Mock()))
class StaticFileHandlerTest(BaseTest):
def test_static_handler(self):
response = self.fetch('/vendors/mopidy/mopidy.js', method='GET')
assert response.code == 200
def test_get_version(self):
assert StaticHandler.get_version(None, None) == Extension.version
class RedirectHandlerTest(BaseTest):
def test_redirect_handler(self):
response = self.fetch('/', method='GET', follow_redirects=False)
assert response.code == 301
response.headers['Location'].endswith('index.html')
class IndexHandlerTest(BaseTest):
def test_index_handler(self):
response = self.fetch('/index.html', method='GET')
assert response.code == 200
def test_get_title(self):
response = self.fetch('/index.html', method='GET')
body = tornado.escape.to_unicode(response.body)
assert '<title>MusicBox on localhost</title>' in body
def test_initialize_sets_dictionary_objects(self):
response = self.fetch('/index.html', method='GET')
body = tornado.escape.to_unicode(response.body)
assert 'data-is-musicbox="true"' in body
assert 'data-has-alarmclock="false"' in body
assert 'data-websocket-url=""' in body

78
tests/test_webclient.py Normal file
View File

@ -0,0 +1,78 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import unittest
import mock
import mopidy.config as mopidy_config
from mopidy_musicbox_webclient import Extension
from mopidy_musicbox_webclient.webclient import Webclient
class WebclientTests(unittest.TestCase):
def setUp(self):
config = mopidy_config.Proxy(
{
'musicbox_webclient': {
'enabled': True,
'musicbox': False,
'websocket_host': 'host_mock',
'websocket_port': 999,
}
})
self.ext = Extension()
self.mmw = Webclient(config)
def test_get_version(self):
assert self.mmw.get_version() == self.ext.version
def test_get_websocket_url_uses_config_file(self):
assert self.mmw.get_websocket_url(mock.Mock()) == 'ws://host_mock:999/mopidy/ws'
def test_get_websocket_url_uses_request_host(self):
config = mopidy_config.Proxy(
{
'musicbox_webclient': {
'enabled': True,
'musicbox': False,
'websocket_host': '',
'websocket_port': 999,
}
})
request_mock = mock.Mock(spec='tornado.HTTPServerRequest')
request_mock.host = '127.0.0.1'
request_mock.protocol = 'https'
self.mmw.config = config
assert self.mmw.get_websocket_url(request_mock) == 'wss://127.0.0.1:999/mopidy/ws'
def test_get_websocket_url_uses_http_port(self):
config = mopidy_config.Proxy(
{
'http': {
'port': 999
},
'musicbox_webclient': {
'enabled': True,
'musicbox': False,
'websocket_host': '127.0.0.1',
'websocket_port': '',
}
})
request_mock = mock.Mock(spec='tornado.HTTPServerRequest')
request_mock.host = '127.0.0.1'
request_mock.protocol = 'https'
self.mmw.config = config
assert self.mmw.get_websocket_url(request_mock) == 'wss://127.0.0.1:999/mopidy/ws'
def test_has_alarmclock(self):
assert not self.mmw.has_alarm_clock()
def test_is_musicbox(self):
assert not self.mmw.is_music_box()

15
tox.ini
View File

@ -2,12 +2,18 @@
envlist = py27, flake8, test, eslint, csslint, tidy envlist = py27, flake8, test, eslint, csslint, tidy
[testenv] [testenv]
sitepackages = true
whitelist_externals =
py.test
deps = deps =
mock mock
mopidy mopidy
pytest pytest
pytest-capturelog
pytest-cov pytest-cov
pytest-xdist pytest-xdist
responses
install_command = pip install --allow-unverified=mopidy --pre {opts} {packages}
commands = commands =
py.test \ py.test \
--basetemp={envtmpdir} \ --basetemp={envtmpdir} \
@ -16,13 +22,17 @@ commands =
{posargs:tests/} {posargs:tests/}
[testenv:flake8] [testenv:flake8]
sitepackages = false
deps = deps =
flake8 flake8
flake8-import-order flake8-import-order
pep8-naming
skip_install = true skip_install = true
commands = flake8 {posargs:mopidy_musicbox_webclient tests} commands = flake8 --show-source --statistics --max-line-length 120 {posargs:mopidy_musicbox_webclient tests}
[testenv:test] [testenv:test]
envdir = py27
sitepackages = false
whitelist_externals = whitelist_externals =
/bin/bash /bin/bash
deps = deps =
@ -33,6 +43,7 @@ commands =
bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm test' bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm test'
[testenv:eslint] [testenv:eslint]
sitepackages = false
whitelist_externals = whitelist_externals =
/bin/bash /bin/bash
deps = deps =
@ -43,6 +54,7 @@ commands =
bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm run eslint' bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm run eslint'
[testenv:csslint] [testenv:csslint]
sitepackages = false
whitelist_externals = whitelist_externals =
/bin/bash /bin/bash
deps = deps =
@ -53,6 +65,7 @@ commands =
bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm run csslint' bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm run csslint'
[testenv:tidy] [testenv:tidy]
sitepackages = false
whitelist_externals = whitelist_externals =
/bin/bash /bin/bash
deps = deps =