Merge lp:~raoul-snyman/openlp/bug-1608194 into lp:openlp

Proposed by Raoul Snyman on 2016-08-13
Status: Merged
Merged at revision: 2694
Proposed branch: lp:~raoul-snyman/openlp/bug-1608194
Merge into: lp:openlp
Diff against target: 1080 lines (+670/-89)
7 files modified
.bzrignore (+1/-0)
openlp/core/ui/media/systemplayer.py (+13/-13)
openlp/plugins/songs/lib/__init__.py (+8/-9)
openlp/plugins/songs/lib/songselect.py (+76/-41)
tests/functional/openlp_core_ui/test_slidecontroller.py (+2/-2)
tests/functional/openlp_core_ui_media/test_systemplayer.py (+529/-0)
tests/functional/openlp_plugins/songs/test_songselect.py (+41/-24)
To merge this branch: bzr merge lp:~raoul-snyman/openlp/bug-1608194
Reviewer Review Type Date Requested Status
Tim Bentley 2016-08-13 Approve on 2016-08-13
Review via email: mp+302867@code.launchpad.net
To post a comment you must log in.
Tim Bentley (trb143) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2016-05-17 08:48:19 +0000
3+++ .bzrignore 2016-08-13 15:05:55 +0000
4@@ -46,3 +46,4 @@
5 coverage
6 tags
7 output
8+htmlcov
9
10=== modified file 'openlp/core/ui/media/systemplayer.py'
11--- openlp/core/ui/media/systemplayer.py 2016-04-13 19:15:53 +0000
12+++ openlp/core/ui/media/systemplayer.py 2016-08-13 15:05:55 +0000
13@@ -83,17 +83,17 @@
14 elif mime_type.startswith('video/'):
15 self._add_to_list(self.video_extensions_list, mime_type)
16
17- def _add_to_list(self, mime_type_list, mimetype):
18+ def _add_to_list(self, mime_type_list, mime_type):
19 """
20 Add mimetypes to the provided list
21 """
22 # Add all extensions which mimetypes provides us for supported types.
23- extensions = mimetypes.guess_all_extensions(str(mimetype))
24+ extensions = mimetypes.guess_all_extensions(mime_type)
25 for extension in extensions:
26 ext = '*%s' % extension
27 if ext not in mime_type_list:
28 mime_type_list.append(ext)
29- log.info('MediaPlugin: %s extensions: %s' % (mimetype, ' '.join(extensions)))
30+ log.info('MediaPlugin: %s extensions: %s', mime_type, ' '.join(extensions))
31
32 def setup(self, display):
33 """
34@@ -284,25 +284,25 @@
35 :return: True if file can be played otherwise False
36 """
37 thread = QtCore.QThread()
38- check_media_player = CheckMedia(path)
39- check_media_player.setVolume(0)
40- check_media_player.moveToThread(thread)
41- check_media_player.finished.connect(thread.quit)
42- thread.started.connect(check_media_player.play)
43+ check_media_worker = CheckMediaWorker(path)
44+ check_media_worker.setVolume(0)
45+ check_media_worker.moveToThread(thread)
46+ check_media_worker.finished.connect(thread.quit)
47+ thread.started.connect(check_media_worker.play)
48 thread.start()
49 while thread.isRunning():
50 self.application.processEvents()
51- return check_media_player.result
52-
53-
54-class CheckMedia(QtMultimedia.QMediaPlayer):
55+ return check_media_worker.result
56+
57+
58+class CheckMediaWorker(QtMultimedia.QMediaPlayer):
59 """
60 Class used to check if a media file is playable
61 """
62 finished = QtCore.pyqtSignal()
63
64 def __init__(self, path):
65- super(CheckMedia, self).__init__(None, QtMultimedia.QMediaPlayer.VideoSurface)
66+ super(CheckMediaWorker, self).__init__(None, QtMultimedia.QMediaPlayer.VideoSurface)
67 self.result = None
68
69 self.error.connect(functools.partial(self.signals, 'error'))
70
71=== modified file 'openlp/plugins/songs/lib/__init__.py'
72--- openlp/plugins/songs/lib/__init__.py 2016-05-27 08:13:14 +0000
73+++ openlp/plugins/songs/lib/__init__.py 2016-08-13 15:05:55 +0000
74@@ -31,9 +31,8 @@
75
76 from openlp.core.common import AppLocation, CONTROL_CHARS
77 from openlp.core.lib import translate
78-from openlp.plugins.songs.lib.db import MediaFile, Song
79-from .db import Author
80-from .ui import SongStrings
81+from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
82+from openlp.plugins.songs.lib.ui import SongStrings
83
84 log = logging.getLogger(__name__)
85
86@@ -315,8 +314,8 @@
87 ]
88 recommended_index = -1
89 if recommendation:
90- for index in range(len(encodings)):
91- if recommendation == encodings[index][0]:
92+ for index, encoding in enumerate(encodings):
93+ if recommendation == encoding[0]:
94 recommended_index = index
95 break
96 if recommended_index > -1:
97@@ -442,7 +441,7 @@
98 # Encoded buffer.
99 ebytes = bytearray()
100 for match in PATTERN.finditer(text):
101- iinu, word, arg, hex, char, brace, tchar = match.groups()
102+ iinu, word, arg, hex_, char, brace, tchar = match.groups()
103 # \x (non-alpha character)
104 if char:
105 if char in '\\{}':
106@@ -450,7 +449,7 @@
107 else:
108 word = char
109 # Flush encoded buffer to output buffer
110- if ebytes and not hex and not tchar:
111+ if ebytes and not hex_ and not tchar:
112 failed = False
113 while True:
114 try:
115@@ -507,11 +506,11 @@
116 elif iinu:
117 ignorable = True
118 # \'xx
119- elif hex:
120+ elif hex_:
121 if curskip > 0:
122 curskip -= 1
123 elif not ignorable:
124- ebytes.append(int(hex, 16))
125+ ebytes.append(int(hex_, 16))
126 elif tchar:
127 if curskip > 0:
128 curskip -= 1
129
130=== modified file 'openlp/plugins/songs/lib/songselect.py'
131--- openlp/plugins/songs/lib/songselect.py 2016-05-27 08:13:14 +0000
132+++ openlp/plugins/songs/lib/songselect.py 2016-08-13 15:05:55 +0000
133@@ -23,7 +23,8 @@
134 The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
135 """
136 import logging
137-import sys
138+import random
139+import re
140 from http.cookiejar import CookieJar
141 from urllib.parse import urlencode
142 from urllib.request import HTTPCookieProcessor, URLError, build_opener
143@@ -32,14 +33,21 @@
144
145 from bs4 import BeautifulSoup, NavigableString
146
147-from openlp.plugins.songs.lib import Song, VerseType, clean_song, Author
148+from openlp.plugins.songs.lib import Song, Author, Topic, VerseType, clean_song
149 from openlp.plugins.songs.lib.openlyricsxml import SongXML
150
151-USER_AGENT = 'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; GT-I9000 ' \
152- 'Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 ' \
153- 'Mobile Safari/534.30'
154-BASE_URL = 'https://mobile.songselect.com'
155-LOGIN_URL = BASE_URL + '/account/login'
156+USER_AGENTS = [
157+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) '
158+ 'Chrome/52.0.2743.116 Safari/537.36',
159+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36',
160+ 'Mozilla/5.0 (X11; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0',
161+ 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
162+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:47.0) Gecko/20100101 Firefox/47.0'
163+]
164+BASE_URL = 'https://songselect.ccli.com'
165+LOGIN_PAGE = 'https://profile.ccli.com/account/signin?appContext=SongSelect&returnUrl='\
166+ 'https%3a%2f%2fsongselect.ccli.com%2f'
167+LOGIN_URL = 'https://profile.ccli.com/'
168 LOGOUT_URL = BASE_URL + '/account/logout'
169 SEARCH_URL = BASE_URL + '/search/results'
170
171@@ -60,7 +68,7 @@
172 self.db_manager = db_manager
173 self.html_parser = HTMLParser()
174 self.opener = build_opener(HTTPCookieProcessor(CookieJar()))
175- self.opener.addheaders = [('User-Agent', USER_AGENT)]
176+ self.opener.addheaders = [('User-Agent', random.choice(USER_AGENTS))]
177 self.run_search = True
178
179 def login(self, username, password, callback=None):
180@@ -76,27 +84,27 @@
181 if callback:
182 callback()
183 try:
184- login_page = BeautifulSoup(self.opener.open(LOGIN_URL).read(), 'lxml')
185- except (TypeError, URLError) as e:
186- log.exception('Could not login to SongSelect, {error}'.format(error=e))
187+ login_page = BeautifulSoup(self.opener.open(LOGIN_PAGE).read(), 'lxml')
188+ except (TypeError, URLError) as error:
189+ log.exception('Could not login to SongSelect, {error}'.format(error=error))
190 return False
191 if callback:
192 callback()
193 token_input = login_page.find('input', attrs={'name': '__RequestVerificationToken'})
194 data = urlencode({
195 '__RequestVerificationToken': token_input['value'],
196- 'UserName': username,
197- 'Password': password,
198+ 'emailAddress': username,
199+ 'password': password,
200 'RememberMe': 'false'
201 })
202 try:
203 posted_page = BeautifulSoup(self.opener.open(LOGIN_URL, data.encode('utf-8')).read(), 'lxml')
204- except (TypeError, URLError) as e:
205- log.exception('Could not login to SongSelect, {error}'.format(error=e))
206+ except (TypeError, URLError) as error:
207+ log.exception('Could not login to SongSelect, {error}'.format(error=error))
208 return False
209 if callback:
210 callback()
211- return not posted_page.find('input', attrs={'name': '__RequestVerificationToken'})
212+ return posted_page.find('input', id='SearchText') is not None
213
214 def logout(self):
215 """
216@@ -104,8 +112,8 @@
217 """
218 try:
219 self.opener.open(LOGOUT_URL)
220- except (TypeError, URLError) as e:
221- log.exception('Could not log of SongSelect, {error}'.format(error=e))
222+ except (TypeError, URLError) as error:
223+ log.exception('Could not log of SongSelect, {error}'.format(error=error))
224
225 def search(self, search_text, max_results, callback=None):
226 """
227@@ -117,7 +125,15 @@
228 :return: List of songs
229 """
230 self.run_search = True
231- params = {'allowredirect': 'false', 'SearchTerm': search_text}
232+ params = {
233+ 'SongContent': '',
234+ 'PrimaryLanguage': '',
235+ 'Keys': '',
236+ 'Themes': '',
237+ 'List': '',
238+ 'Sort': '',
239+ 'SearchText': search_text
240+ }
241 current_page = 1
242 songs = []
243 while self.run_search:
244@@ -125,17 +141,17 @@
245 params['page'] = current_page
246 try:
247 results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + urlencode(params)).read(), 'lxml')
248- search_results = results_page.find_all('li', 'result pane')
249- except (TypeError, URLError) as e:
250- log.exception('Could not search SongSelect, {error}'.format(error=e))
251+ search_results = results_page.find_all('div', 'song-result')
252+ except (TypeError, URLError) as error:
253+ log.exception('Could not search SongSelect, {error}'.format(error=error))
254 search_results = None
255 if not search_results:
256 break
257 for result in search_results:
258 song = {
259- 'title': unescape(result.find('h3').string),
260- 'authors': [unescape(author.string) for author in result.find_all('li')],
261- 'link': BASE_URL + result.find('a')['href']
262+ 'title': unescape(result.find('p', 'song-result-title').find('a').string).strip(),
263+ 'authors': unescape(result.find('p', 'song-result-subtitle').string).strip().split(', '),
264+ 'link': BASE_URL + result.find('p', 'song-result-title').find('a')['href']
265 }
266 if callback:
267 callback(song)
268@@ -157,33 +173,43 @@
269 callback()
270 try:
271 song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
272- except (TypeError, URLError) as e:
273- log.exception('Could not get song from SongSelect, {error}'.format(error=e))
274+ except (TypeError, URLError) as error:
275+ log.exception('Could not get song from SongSelect, {error}'.format(error=error))
276 return None
277 if callback:
278 callback()
279 try:
280- lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml')
281+ lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/viewlyrics').read(), 'lxml')
282 except (TypeError, URLError):
283 log.exception('Could not get lyrics from SongSelect')
284 return None
285 if callback:
286 callback()
287- song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')])
288- song['copyright'] = unescape(song['copyright'])
289- song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip()
290+ copyright_elements = []
291+ theme_elements = []
292+ copyrights_regex = re.compile(r'\bCopyrights\b')
293+ themes_regex = re.compile(r'\bThemes\b')
294+ for ul in song_page.find_all('ul', 'song-meta-list'):
295+ if ul.find('li', string=copyrights_regex):
296+ copyright_elements.extend(ul.find_all('li')[1:])
297+ if ul.find('li', string=themes_regex):
298+ theme_elements.extend(ul.find_all('li')[1:])
299+ song['copyright'] = '/'.join([unescape(li.string).strip() for li in copyright_elements])
300+ song['topics'] = [unescape(li.string).strip() for li in theme_elements]
301+ song['ccli_number'] = song_page.find('div', 'song-content-data').find('ul').find('li')\
302+ .find('strong').string.strip()
303 song['verses'] = []
304- verses = lyrics_page.find('section', 'lyrics').find_all('p')
305- verse_labels = lyrics_page.find('section', 'lyrics').find_all('h3')
306- for counter in range(len(verses)):
307- verse = {'label': verse_labels[counter].string, 'lyrics': ''}
308- for v in verses[counter].contents:
309+ verses = lyrics_page.find('div', 'song-viewer lyrics').find_all('p')
310+ verse_labels = lyrics_page.find('div', 'song-viewer lyrics').find_all('h3')
311+ for verse, label in zip(verses, verse_labels):
312+ song_verse = {'label': unescape(label.string).strip(), 'lyrics': ''}
313+ for v in verse.contents:
314 if isinstance(v, NavigableString):
315- verse['lyrics'] = verse['lyrics'] + v.string
316+ song_verse['lyrics'] += unescape(v.string).strip()
317 else:
318- verse['lyrics'] += '\n'
319- verse['lyrics'] = verse['lyrics'].strip(' \n\r\t')
320- song['verses'].append(unescape(verse))
321+ song_verse['lyrics'] += '\n'
322+ song_verse['lyrics'] = song_verse['lyrics'].strip(' \n\r\t')
323+ song['verses'].append(song_verse)
324 for counter, author in enumerate(song['authors']):
325 song['authors'][counter] = unescape(author)
326 return song
327@@ -199,7 +225,11 @@
328 song_xml = SongXML()
329 verse_order = []
330 for verse in song['verses']:
331- verse_type, verse_number = verse['label'].split(' ')[:2]
332+ if ' ' in verse['label']:
333+ verse_type, verse_number = verse['label'].split(' ', 1)
334+ else:
335+ verse_type = verse['label']
336+ verse_number = 1
337 verse_type = VerseType.from_loose_input(verse_type)
338 verse_number = int(verse_number)
339 song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics'])
340@@ -220,6 +250,11 @@
341 last_name = name_parts[1]
342 author = Author.populate(first_name=first_name, last_name=last_name, display_name=author_name)
343 db_song.add_author(author)
344+ for topic_name in song.get('topics', []):
345+ topic = self.db_manager.get_object_filtered(Topic, Topic.name == topic_name)
346+ if not topic:
347+ topic = Topic.populate(name=topic_name)
348+ db_song.topics.append(topic)
349 self.db_manager.save_object(db_song)
350 return db_song
351
352
353=== modified file 'tests/functional/openlp_core_ui/test_slidecontroller.py'
354--- tests/functional/openlp_core_ui/test_slidecontroller.py 2016-07-16 16:51:08 +0000
355+++ tests/functional/openlp_core_ui/test_slidecontroller.py 2016-08-13 15:05:55 +0000
356@@ -243,7 +243,7 @@
357 mocked_service_item = MagicMock()
358 mocked_service_item.from_service = False
359 mocked_preview_widget.current_slide_number.return_value = 1
360- mocked_preview_widget.slide_count.return_value = 2
361+ mocked_preview_widget.slide_count = MagicMock(return_value=2)
362 mocked_live_controller.preview_widget = MagicMock()
363 Registry.create()
364 Registry().register('live_controller', mocked_live_controller)
365@@ -273,7 +273,7 @@
366 mocked_service_item.from_service = True
367 mocked_service_item.unique_identifier = 42
368 mocked_preview_widget.current_slide_number.return_value = 1
369- mocked_preview_widget.slide_count.return_value = 2
370+ mocked_preview_widget.slide_count = MagicMock(return_value=2)
371 mocked_live_controller.preview_widget = MagicMock()
372 Registry.create()
373 Registry().register('live_controller', mocked_live_controller)
374
375=== added file 'tests/functional/openlp_core_ui_media/test_systemplayer.py'
376--- tests/functional/openlp_core_ui_media/test_systemplayer.py 1970-01-01 00:00:00 +0000
377+++ tests/functional/openlp_core_ui_media/test_systemplayer.py 2016-08-13 15:05:55 +0000
378@@ -0,0 +1,529 @@
379+# -*- coding: utf-8 -*-
380+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
381+
382+###############################################################################
383+# OpenLP - Open Source Lyrics Projection #
384+# --------------------------------------------------------------------------- #
385+# Copyright (c) 2008-2016 OpenLP Developers #
386+# --------------------------------------------------------------------------- #
387+# This program is free software; you can redistribute it and/or modify it #
388+# under the terms of the GNU General Public License as published by the Free #
389+# Software Foundation; version 2 of the License. #
390+# #
391+# This program is distributed in the hope that it will be useful, but WITHOUT #
392+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
393+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
394+# more details. #
395+# #
396+# You should have received a copy of the GNU General Public License along #
397+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
398+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
399+###############################################################################
400+"""
401+Package to test the openlp.core.ui.media.systemplayer package.
402+"""
403+from unittest import TestCase
404+
405+from PyQt5 import QtCore, QtMultimedia
406+
407+from openlp.core.common import Registry
408+from openlp.core.ui.media import MediaState
409+from openlp.core.ui.media.systemplayer import SystemPlayer, CheckMediaWorker, ADDITIONAL_EXT
410+
411+from tests.functional import MagicMock, call, patch
412+
413+
414+class TestSystemPlayer(TestCase):
415+ """
416+ Test the system media player
417+ """
418+ @patch('openlp.core.ui.media.systemplayer.mimetypes')
419+ @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
420+ def test_constructor(self, MockQMediaPlayer, mocked_mimetypes):
421+ """
422+ Test the SystemPlayer constructor
423+ """
424+ # GIVEN: The SystemPlayer class and a mockedQMediaPlayer
425+ mocked_media_player = MagicMock()
426+ mocked_media_player.supportedMimeTypes.return_value = [
427+ 'application/postscript',
428+ 'audio/aiff',
429+ 'audio/x-aiff',
430+ 'text/html',
431+ 'video/animaflex',
432+ 'video/x-ms-asf'
433+ ]
434+ mocked_mimetypes.guess_all_extensions.side_effect = [
435+ ['.aiff'],
436+ ['.aiff'],
437+ ['.afl'],
438+ ['.asf']
439+ ]
440+ MockQMediaPlayer.return_value = mocked_media_player
441+
442+ # WHEN: An object is created from it
443+ player = SystemPlayer(self)
444+
445+ # THEN: The correct initial values should be set up
446+ self.assertEqual('system', player.name)
447+ self.assertEqual('System', player.original_name)
448+ self.assertEqual('&System', player.display_name)
449+ self.assertEqual(self, player.parent)
450+ self.assertEqual(ADDITIONAL_EXT, player.additional_extensions)
451+ MockQMediaPlayer.assert_called_once_with(None, QtMultimedia.QMediaPlayer.VideoSurface)
452+ mocked_mimetypes.init.assert_called_once_with()
453+ mocked_media_player.service.assert_called_once_with()
454+ mocked_media_player.supportedMimeTypes.assert_called_once_with()
455+ self.assertEqual(['*.aiff'], player.audio_extensions_list)
456+ self.assertEqual(['*.afl', '*.asf'], player.video_extensions_list)
457+
458+ @patch('openlp.core.ui.media.systemplayer.QtMultimediaWidgets.QVideoWidget')
459+ @patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
460+ def test_setup(self, MockQMediaPlayer, MockQVideoWidget):
461+ """
462+ Test the setup() method of SystemPlayer
463+ """
464+ # GIVEN: A SystemPlayer instance and a mock display
465+ player = SystemPlayer(self)
466+ mocked_display = MagicMock()
467+ mocked_display.size.return_value = [1, 2, 3, 4]
468+ mocked_video_widget = MagicMock()
469+ mocked_media_player = MagicMock()
470+ MockQVideoWidget.return_value = mocked_video_widget
471+ MockQMediaPlayer.return_value = mocked_media_player
472+
473+ # WHEN: setup() is run
474+ player.setup(mocked_display)
475+
476+ # THEN: The player should have a display widget
477+ MockQVideoWidget.assert_called_once_with(mocked_display)
478+ self.assertEqual(mocked_video_widget, mocked_display.video_widget)
479+ mocked_display.size.assert_called_once_with()
480+ mocked_video_widget.resize.assert_called_once_with([1, 2, 3, 4])
481+ MockQMediaPlayer.assert_called_with(mocked_display)
482+ self.assertEqual(mocked_media_player, mocked_display.media_player)
483+ mocked_media_player.setVideoOutput.assert_called_once_with(mocked_video_widget)
484+ mocked_video_widget.raise_.assert_called_once_with()
485+ mocked_video_widget.hide.assert_called_once_with()
486+ self.assertTrue(player.has_own_widget)
487+
488+ def test_check_available(self):
489+ """
490+ Test the check_available() method on SystemPlayer
491+ """
492+ # GIVEN: A SystemPlayer instance
493+ player = SystemPlayer(self)
494+
495+ # WHEN: check_available is run
496+ result = player.check_available()
497+
498+ # THEN: it should be available
499+ self.assertTrue(result)
500+
501+ def test_load_valid_media(self):
502+ """
503+ Test the load() method of SystemPlayer with a valid media file
504+ """
505+ # GIVEN: A SystemPlayer instance and a mocked display
506+ player = SystemPlayer(self)
507+ mocked_display = MagicMock()
508+ mocked_display.controller.media_info.volume = 1
509+ mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
510+
511+ # WHEN: The load() method is run
512+ with patch.object(player, 'check_media') as mocked_check_media, \
513+ patch.object(player, 'volume') as mocked_volume:
514+ mocked_check_media.return_value = True
515+ result = player.load(mocked_display)
516+
517+ # THEN: the file is sent to the video widget
518+ mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
519+ mocked_check_media.assert_called_once_with('/path/to/file')
520+ mocked_display.media_player.setMedia.assert_called_once_with(
521+ QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile('/path/to/file')))
522+ mocked_volume.assert_called_once_with(mocked_display, 1)
523+ self.assertTrue(result)
524+
525+ def test_load_invalid_media(self):
526+ """
527+ Test the load() method of SystemPlayer with an invalid media file
528+ """
529+ # GIVEN: A SystemPlayer instance and a mocked display
530+ player = SystemPlayer(self)
531+ mocked_display = MagicMock()
532+ mocked_display.controller.media_info.volume = 1
533+ mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
534+
535+ # WHEN: The load() method is run
536+ with patch.object(player, 'check_media') as mocked_check_media, \
537+ patch.object(player, 'volume') as mocked_volume:
538+ mocked_check_media.return_value = False
539+ result = player.load(mocked_display)
540+
541+ # THEN: stuff
542+ mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
543+ mocked_check_media.assert_called_once_with('/path/to/file')
544+ self.assertFalse(result)
545+
546+ def test_resize(self):
547+ """
548+ Test the resize() method of the SystemPlayer
549+ """
550+ # GIVEN: A SystemPlayer instance and a mocked display
551+ player = SystemPlayer(self)
552+ mocked_display = MagicMock()
553+ mocked_display.size.return_value = [1, 2, 3, 4]
554+
555+ # WHEN: The resize() method is called
556+ player.resize(mocked_display)
557+
558+ # THEN: The player is resized
559+ mocked_display.size.assert_called_once_with()
560+ mocked_display.video_widget.resize.assert_called_once_with([1, 2, 3, 4])
561+
562+ @patch('openlp.core.ui.media.systemplayer.functools')
563+ def test_play_is_live(self, mocked_functools):
564+ """
565+ Test the play() method of the SystemPlayer on the live display
566+ """
567+ # GIVEN: A SystemPlayer instance and a mocked display
568+ mocked_functools.partial.return_value = 'function'
569+ player = SystemPlayer(self)
570+ mocked_display = MagicMock()
571+ mocked_display.controller.is_live = True
572+ mocked_display.controller.media_info.start_time = 1
573+ mocked_display.controller.media_info.volume = 1
574+
575+ # WHEN: play() is called
576+ with patch.object(player, 'get_live_state') as mocked_get_live_state, \
577+ patch.object(player, 'seek') as mocked_seek, \
578+ patch.object(player, 'volume') as mocked_volume, \
579+ patch.object(player, 'set_state') as mocked_set_state:
580+ mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
581+ result = player.play(mocked_display)
582+
583+ # THEN: the media file is played
584+ mocked_get_live_state.assert_called_once_with()
585+ mocked_display.media_player.play.assert_called_once_with()
586+ mocked_seek.assert_called_once_with(mocked_display, 1000)
587+ mocked_volume.assert_called_once_with(mocked_display, 1)
588+ mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
589+ mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
590+ mocked_display.video_widget.raise_.assert_called_once_with()
591+ self.assertTrue(result)
592+
593+ @patch('openlp.core.ui.media.systemplayer.functools')
594+ def test_play_is_preview(self, mocked_functools):
595+ """
596+ Test the play() method of the SystemPlayer on the preview display
597+ """
598+ # GIVEN: A SystemPlayer instance and a mocked display
599+ mocked_functools.partial.return_value = 'function'
600+ player = SystemPlayer(self)
601+ mocked_display = MagicMock()
602+ mocked_display.controller.is_live = False
603+ mocked_display.controller.media_info.start_time = 1
604+ mocked_display.controller.media_info.volume = 1
605+
606+ # WHEN: play() is called
607+ with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
608+ patch.object(player, 'seek') as mocked_seek, \
609+ patch.object(player, 'volume') as mocked_volume, \
610+ patch.object(player, 'set_state') as mocked_set_state:
611+ mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
612+ result = player.play(mocked_display)
613+
614+ # THEN: the media file is played
615+ mocked_get_preview_state.assert_called_once_with()
616+ mocked_display.media_player.play.assert_called_once_with()
617+ mocked_seek.assert_called_once_with(mocked_display, 1000)
618+ mocked_volume.assert_called_once_with(mocked_display, 1)
619+ mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
620+ mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
621+ mocked_display.video_widget.raise_.assert_called_once_with()
622+ self.assertTrue(result)
623+
624+ def test_pause_is_live(self):
625+ """
626+ Test the pause() method of the SystemPlayer on the live display
627+ """
628+ # GIVEN: A SystemPlayer instance
629+ player = SystemPlayer(self)
630+ mocked_display = MagicMock()
631+ mocked_display.controller.is_live = True
632+
633+ # WHEN: The pause method is called
634+ with patch.object(player, 'get_live_state') as mocked_get_live_state, \
635+ patch.object(player, 'set_state') as mocked_set_state:
636+ mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PausedState
637+ player.pause(mocked_display)
638+
639+ # THEN: The video is paused
640+ mocked_display.media_player.pause.assert_called_once_with()
641+ mocked_get_live_state.assert_called_once_with()
642+ mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
643+
644+ def test_pause_is_preview(self):
645+ """
646+ Test the pause() method of the SystemPlayer on the preview display
647+ """
648+ # GIVEN: A SystemPlayer instance
649+ player = SystemPlayer(self)
650+ mocked_display = MagicMock()
651+ mocked_display.controller.is_live = False
652+
653+ # WHEN: The pause method is called
654+ with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
655+ patch.object(player, 'set_state') as mocked_set_state:
656+ mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PausedState
657+ player.pause(mocked_display)
658+
659+ # THEN: The video is paused
660+ mocked_display.media_player.pause.assert_called_once_with()
661+ mocked_get_preview_state.assert_called_once_with()
662+ mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
663+
664+ def test_stop(self):
665+ """
666+ Test the stop() method of the SystemPlayer
667+ """
668+ # GIVEN: A SystemPlayer instance
669+ player = SystemPlayer(self)
670+ mocked_display = MagicMock()
671+
672+ # WHEN: The stop method is called
673+ with patch.object(player, 'set_visible') as mocked_set_visible, \
674+ patch.object(player, 'set_state') as mocked_set_state:
675+ player.stop(mocked_display)
676+
677+ # THEN: The video is stopped
678+ mocked_display.media_player.stop.assert_called_once_with()
679+ mocked_set_visible.assert_called_once_with(mocked_display, False)
680+ mocked_set_state.assert_called_once_with(MediaState.Stopped, mocked_display)
681+
682+ def test_volume(self):
683+ """
684+ Test the volume() method of the SystemPlayer
685+ """
686+ # GIVEN: A SystemPlayer instance
687+ player = SystemPlayer(self)
688+ mocked_display = MagicMock()
689+ mocked_display.has_audio = True
690+
691+ # WHEN: The stop method is called
692+ player.volume(mocked_display, 2)
693+
694+ # THEN: The video is stopped
695+ mocked_display.media_player.setVolume.assert_called_once_with(2)
696+
697+ def test_seek(self):
698+ """
699+ Test the seek() method of the SystemPlayer
700+ """
701+ # GIVEN: A SystemPlayer instance
702+ player = SystemPlayer(self)
703+ mocked_display = MagicMock()
704+
705+ # WHEN: The stop method is called
706+ player.seek(mocked_display, 2)
707+
708+ # THEN: The video is stopped
709+ mocked_display.media_player.setPosition.assert_called_once_with(2)
710+
711+ def test_reset(self):
712+ """
713+ Test the reset() method of the SystemPlayer
714+ """
715+ # GIVEN: A SystemPlayer instance
716+ player = SystemPlayer(self)
717+ mocked_display = MagicMock()
718+
719+ # WHEN: reset() is called
720+ with patch.object(player, 'set_state') as mocked_set_state, \
721+ patch.object(player, 'set_visible') as mocked_set_visible:
722+ player.reset(mocked_display)
723+
724+ # THEN: The media player is reset
725+ mocked_display.media_player.stop()
726+ mocked_display.media_player.setMedia.assert_called_once_with(QtMultimedia.QMediaContent())
727+ mocked_set_visible.assert_called_once_with(mocked_display, False)
728+ mocked_display.video_widget.setVisible.assert_called_once_with(False)
729+ mocked_set_state.assert_called_once_with(MediaState.Off, mocked_display)
730+
731+ def test_set_visible(self):
732+ """
733+ Test the set_visible() method on the SystemPlayer
734+ """
735+ # GIVEN: A SystemPlayer instance and a mocked display
736+ player = SystemPlayer(self)
737+ player.has_own_widget = True
738+ mocked_display = MagicMock()
739+
740+ # WHEN: set_visible() is called
741+ player.set_visible(mocked_display, True)
742+
743+ # THEN: The widget should be visible
744+ mocked_display.video_widget.setVisible.assert_called_once_with(True)
745+
746+ def test_set_duration(self):
747+ """
748+ Test the set_duration() method of the SystemPlayer
749+ """
750+ # GIVEN: a mocked controller
751+ mocked_controller = MagicMock()
752+ mocked_controller.media_info.length = 5
753+
754+ # WHEN: The set_duration() is called. NB: the 10 here is ignored by the code
755+ SystemPlayer.set_duration(mocked_controller, 10)
756+
757+ # THEN: The maximum length of the slider should be set
758+ mocked_controller.seek_slider.setMaximum.assert_called_once_with(5)
759+
760+ def test_update_ui(self):
761+ """
762+ Test the update_ui() method on the SystemPlayer
763+ """
764+ # GIVEN: A SystemPlayer instance
765+ player = SystemPlayer(self)
766+ player.state = MediaState.Playing
767+ mocked_display = MagicMock()
768+ mocked_display.media_player.state.return_value = QtMultimedia.QMediaPlayer.PausedState
769+ mocked_display.controller.media_info.end_time = 1
770+ mocked_display.media_player.position.return_value = 2
771+ mocked_display.controller.seek_slider.isSliderDown.return_value = False
772+
773+ # WHEN: update_ui() is called
774+ with patch.object(player, 'stop') as mocked_stop, \
775+ patch.object(player, 'set_visible') as mocked_set_visible:
776+ player.update_ui(mocked_display)
777+
778+ # THEN: The UI is updated
779+ expected_stop_calls = [call(mocked_display), call(mocked_display)]
780+ expected_position_calls = [call(), call()]
781+ expected_block_signals_calls = [call(True), call(False)]
782+ mocked_display.media_player.state.assert_called_once_with()
783+ self.assertEqual(2, mocked_stop.call_count)
784+ self.assertEqual(expected_stop_calls, mocked_stop.call_args_list)
785+ self.assertEqual(2, mocked_display.media_player.position.call_count)
786+ self.assertEqual(expected_position_calls, mocked_display.media_player.position.call_args_list)
787+ mocked_set_visible.assert_called_once_with(mocked_display, False)
788+ mocked_display.controller.seek_slider.isSliderDown.assert_called_once_with()
789+ self.assertEqual(expected_block_signals_calls,
790+ mocked_display.controller.seek_slider.blockSignals.call_args_list)
791+ mocked_display.controller.seek_slider.setSliderPosition.assert_called_once_with(2)
792+
793+ def test_get_media_display_css(self):
794+ """
795+ Test the get_media_display_css() method of the SystemPlayer
796+ """
797+ # GIVEN: A SystemPlayer instance
798+ player = SystemPlayer(self)
799+
800+ # WHEN: get_media_display_css() is called
801+ result = player.get_media_display_css()
802+
803+ # THEN: The css should be empty
804+ self.assertEqual('', result)
805+
806+ def test_get_info(self):
807+ """
808+ Test the get_info() method of the SystemPlayer
809+ """
810+ # GIVEN: A SystemPlayer instance
811+ player = SystemPlayer(self)
812+
813+ # WHEN: get_info() is called
814+ result = player.get_info()
815+
816+ # THEN: The info should be correct
817+ expected_info = 'This media player uses your operating system to provide media capabilities.<br/> ' \
818+ '<strong>Audio</strong><br/>[]<br/><strong>Video</strong><br/>[]<br/>'
819+ self.assertEqual(expected_info, result)
820+
821+ @patch('openlp.core.ui.media.systemplayer.CheckMediaWorker')
822+ @patch('openlp.core.ui.media.systemplayer.QtCore.QThread')
823+ def test_check_media(self, MockQThread, MockCheckMediaWorker):
824+ """
825+ Test the check_media() method of the SystemPlayer
826+ """
827+ # GIVEN: A SystemPlayer instance and a mocked thread
828+ valid_file = '/path/to/video.ogv'
829+ mocked_application = MagicMock()
830+ Registry().create()
831+ Registry().register('application', mocked_application)
832+ player = SystemPlayer(self)
833+ mocked_thread = MagicMock()
834+ mocked_thread.isRunning.side_effect = [True, False]
835+ mocked_thread.quit = 'quit' # actually supposed to be a slot, but it's all mocked out anyway
836+ MockQThread.return_value = mocked_thread
837+ mocked_check_media_worker = MagicMock()
838+ mocked_check_media_worker.play = 'play'
839+ mocked_check_media_worker.result = True
840+ MockCheckMediaWorker.return_value = mocked_check_media_worker
841+
842+ # WHEN: check_media() is called with a valid media file
843+ result = player.check_media(valid_file)
844+
845+ # THEN: It should return True
846+ MockQThread.assert_called_once_with()
847+ MockCheckMediaWorker.assert_called_once_with(valid_file)
848+ mocked_check_media_worker.setVolume.assert_called_once_with(0)
849+ mocked_check_media_worker.moveToThread.assert_called_once_with(mocked_thread)
850+ mocked_check_media_worker.finished.connect.assert_called_once_with('quit')
851+ mocked_thread.started.connect.assert_called_once_with('play')
852+ mocked_thread.start.assert_called_once_with()
853+ self.assertEqual(2, mocked_thread.isRunning.call_count)
854+ mocked_application.processEvents.assert_called_once_with()
855+ self.assertTrue(result)
856+
857+
858+class TestCheckMediaWorker(TestCase):
859+ """
860+ Test the CheckMediaWorker class
861+ """
862+ def test_constructor(self):
863+ """
864+ Test the constructor of the CheckMediaWorker class
865+ """
866+ # GIVEN: A file path
867+ path = 'file.ogv'
868+
869+ # WHEN: The CheckMediaWorker object is instantiated
870+ worker = CheckMediaWorker(path)
871+
872+ # THEN: The correct values should be set up
873+ self.assertIsNotNone(worker)
874+
875+ def test_signals_media(self):
876+ """
877+ Test the signals() signal of the CheckMediaWorker class with a "media" origin
878+ """
879+ # GIVEN: A CheckMediaWorker instance
880+ worker = CheckMediaWorker('file.ogv')
881+
882+ # WHEN: signals() is called with media and BufferedMedia
883+ with patch.object(worker, 'stop') as mocked_stop, \
884+ patch.object(worker, 'finished') as mocked_finished:
885+ worker.signals('media', worker.BufferedMedia)
886+
887+ # THEN: The worker should exit and the result should be True
888+ mocked_stop.assert_called_once_with()
889+ mocked_finished.emit.assert_called_once_with()
890+ self.assertTrue(worker.result)
891+
892+ def test_signals_error(self):
893+ """
894+ Test the signals() signal of the CheckMediaWorker class with a "error" origin
895+ """
896+ # GIVEN: A CheckMediaWorker instance
897+ worker = CheckMediaWorker('file.ogv')
898+
899+ # WHEN: signals() is called with error and BufferedMedia
900+ with patch.object(worker, 'stop') as mocked_stop, \
901+ patch.object(worker, 'finished') as mocked_finished:
902+ worker.signals('error', None)
903+
904+ # THEN: The worker should exit and the result should be True
905+ mocked_stop.assert_called_once_with()
906+ mocked_finished.emit.assert_called_once_with()
907+ self.assertFalse(worker.result)
908
909=== modified file 'tests/functional/openlp_plugins/songs/test_songselect.py'
910--- tests/functional/openlp_plugins/songs/test_songselect.py 2016-05-31 21:40:13 +0000
911+++ tests/functional/openlp_plugins/songs/test_songselect.py 2016-08-13 15:05:55 +0000
912@@ -1,5 +1,6 @@
913 # -*- coding: utf-8 -*-
914 # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
915+# pylint: disable=protected-access
916
917 ###############################################################################
918 # OpenLP - Open Source Lyrics Projection #
919@@ -28,14 +29,13 @@
920
921 from PyQt5 import QtWidgets
922
923-from tests.helpers.songfileimport import SongImportTestHelper
924 from openlp.core import Registry
925 from openlp.plugins.songs.forms.songselectform import SongSelectForm, SearchWorker
926 from openlp.plugins.songs.lib import Song
927 from openlp.plugins.songs.lib.songselect import SongSelectImport, LOGOUT_URL, BASE_URL
928-from openlp.plugins.songs.lib.importers.cclifile import CCLIFileImport
929
930 from tests.functional import MagicMock, patch, call
931+from tests.helpers.songfileimport import SongImportTestHelper
932 from tests.helpers.testmixin import TestMixin
933
934 TEST_PATH = os.path.abspath(
935@@ -71,7 +71,7 @@
936 mocked_opener = MagicMock()
937 mocked_build_opener.return_value = mocked_opener
938 mocked_login_page = MagicMock()
939- mocked_login_page.find.return_value = {'value': 'blah'}
940+ mocked_login_page.find.side_effect = [{'value': 'blah'}, None]
941 MockedBeautifulSoup.return_value = mocked_login_page
942 mock_callback = MagicMock()
943 importer = SongSelectImport(None)
944@@ -112,7 +112,7 @@
945 mocked_opener = MagicMock()
946 mocked_build_opener.return_value = mocked_opener
947 mocked_login_page = MagicMock()
948- mocked_login_page.find.side_effect = [{'value': 'blah'}, None]
949+ mocked_login_page.find.side_effect = [{'value': 'blah'}, MagicMock()]
950 MockedBeautifulSoup.return_value = mocked_login_page
951 mock_callback = MagicMock()
952 importer = SongSelectImport(None)
953@@ -165,7 +165,7 @@
954 self.assertEqual(0, mock_callback.call_count, 'callback should not have been called')
955 self.assertEqual(1, mocked_opener.open.call_count, 'open should have been called once')
956 self.assertEqual(1, mocked_results_page.find_all.call_count, 'find_all should have been called once')
957- mocked_results_page.find_all.assert_called_with('li', 'result pane')
958+ mocked_results_page.find_all.assert_called_with('div', 'song-result')
959 self.assertEqual([], results, 'The search method should have returned an empty list')
960
961 @patch('openlp.plugins.songs.lib.songselect.build_opener')
962@@ -177,12 +177,18 @@
963 # GIVEN: A bunch of mocked out stuff and an importer object
964 # first search result
965 mocked_result1 = MagicMock()
966- mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
967- mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')]
968+ mocked_result1.find.side_effect = [
969+ MagicMock(find=MagicMock(return_value=MagicMock(string='Title 1'))),
970+ MagicMock(string='James, John'),
971+ MagicMock(find=MagicMock(return_value={'href': '/url1'}))
972+ ]
973 # second search result
974 mocked_result2 = MagicMock()
975- mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}]
976- mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')]
977+ mocked_result2.find.side_effect = [
978+ MagicMock(find=MagicMock(return_value=MagicMock(string='Title 2'))),
979+ MagicMock(string='Philip'),
980+ MagicMock(find=MagicMock(return_value={'href': '/url2'}))
981+ ]
982 # rest of the stuff
983 mocked_opener = MagicMock()
984 mocked_build_opener.return_value = mocked_opener
985@@ -199,10 +205,10 @@
986 self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
987 self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
988 self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
989- mocked_results_page.find_all.assert_called_with('li', 'result pane')
990+ mocked_results_page.find_all.assert_called_with('div', 'song-result')
991 expected_list = [
992- {'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'},
993- {'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}
994+ {'title': 'Title 1', 'authors': ['James', 'John'], 'link': BASE_URL + '/url1'},
995+ {'title': 'Title 2', 'authors': ['Philip'], 'link': BASE_URL + '/url2'}
996 ]
997 self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
998
999@@ -215,16 +221,25 @@
1000 # GIVEN: A bunch of mocked out stuff and an importer object
1001 # first search result
1002 mocked_result1 = MagicMock()
1003- mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
1004- mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')]
1005+ mocked_result1.find.side_effect = [
1006+ MagicMock(find=MagicMock(return_value=MagicMock(string='Title 1'))),
1007+ MagicMock(string='James, John'),
1008+ MagicMock(find=MagicMock(return_value={'href': '/url1'}))
1009+ ]
1010 # second search result
1011 mocked_result2 = MagicMock()
1012- mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}]
1013- mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')]
1014+ mocked_result2.find.side_effect = [
1015+ MagicMock(find=MagicMock(return_value=MagicMock(string='Title 2'))),
1016+ MagicMock(string='Philip'),
1017+ MagicMock(find=MagicMock(return_value={'href': '/url2'}))
1018+ ]
1019 # third search result
1020 mocked_result3 = MagicMock()
1021- mocked_result3.find.side_effect = [MagicMock(string='Title 3'), {'href': '/url3'}]
1022- mocked_result3.find_all.return_value = [MagicMock(string='Author 3-1'), MagicMock(string='Author 3-2')]
1023+ mocked_result3.find.side_effect = [
1024+ MagicMock(find=MagicMock(return_value=MagicMock(string='Title 3'))),
1025+ MagicMock(string='Luke, Matthew'),
1026+ MagicMock(find=MagicMock(return_value={'href': '/url3'}))
1027+ ]
1028 # rest of the stuff
1029 mocked_opener = MagicMock()
1030 mocked_build_opener.return_value = mocked_opener
1031@@ -241,9 +256,9 @@
1032 self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
1033 self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
1034 self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
1035- mocked_results_page.find_all.assert_called_with('li', 'result pane')
1036- expected_list = [{'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'},
1037- {'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}]
1038+ mocked_results_page.find_all.assert_called_with('div', 'song-result')
1039+ expected_list = [{'title': 'Title 1', 'authors': ['James', 'John'], 'link': BASE_URL + '/url1'},
1040+ {'title': 'Title 2', 'authors': ['Philip'], 'link': BASE_URL + '/url2'}]
1041 self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
1042
1043 @patch('openlp.plugins.songs.lib.songselect.build_opener')
1044@@ -337,7 +352,7 @@
1045 self.assertIsNotNone(result, 'The get_song() method should have returned a song dictionary')
1046 self.assertEqual(2, mocked_lyrics_page.find.call_count, 'The find() method should have been called twice')
1047 self.assertEqual(2, mocked_find_all.call_count, 'The find_all() method should have been called twice')
1048- self.assertEqual([call('section', 'lyrics'), call('section', 'lyrics')],
1049+ self.assertEqual([call('div', 'song-viewer lyrics'), call('div', 'song-viewer lyrics')],
1050 mocked_lyrics_page.find.call_args_list,
1051 'The find() method should have been called with the right arguments')
1052 self.assertEqual([call('p'), call('h3')], mocked_find_all.call_args_list,
1053@@ -348,8 +363,9 @@
1054 self.assertEqual(3, len(result['verses']), 'Three verses should have been returned')
1055
1056 @patch('openlp.plugins.songs.lib.songselect.clean_song')
1057+ @patch('openlp.plugins.songs.lib.songselect.Topic')
1058 @patch('openlp.plugins.songs.lib.songselect.Author')
1059- def test_save_song_new_author(self, MockedAuthor, mocked_clean_song):
1060+ def test_save_song_new_author(self, MockedAuthor, MockedTopic, mocked_clean_song):
1061 """
1062 Test that saving a song with a new author performs the correct actions
1063 """
1064@@ -366,6 +382,7 @@
1065 'ccli_number': '123456'
1066 }
1067 MockedAuthor.display_name.__eq__.return_value = False
1068+ MockedTopic.name.__eq__.return_value = False
1069 mocked_db_manager = MagicMock()
1070 mocked_db_manager.get_object_filtered.return_value = None
1071 importer = SongSelectImport(mocked_db_manager)
1072@@ -848,7 +865,7 @@
1073
1074 # WHEN: The start() method is called
1075 with patch.object(worker, 'found_song') as mocked_found_song:
1076- worker._found_song_callback(song)
1077+ worker._found_song_callback(song) # pylint: disable=protected-access
1078
1079 # THEN: The "found_song" signal should have been emitted
1080 mocked_found_song.emit.assert_called_with(song)