openlp/tests/openlp_plugins/songs/test_songselect.py

1009 lines
44 KiB
Python

# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2023 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module contains tests for the CCLI SongSelect importer.
It needs re-writing at some point to load real HTML pages from disk and
then test the behaviour based on those. That way if and when CCLI change
their page layout, changing the tests would just be a case of
re-downloading the HTML pages and changing the code to use the new layout.
"""
from unittest import TestCase
from unittest.mock import MagicMock, patch, sentinel
from PyQt5 import QtWidgets, QtCore
from openlp.core.common.registry import Registry
from openlp.plugins.songs.forms.songselectform import SongSelectForm
from openlp.plugins.songs.lib import Song
from openlp.plugins.songs.lib.songselect import BASE_URL, LOGIN_PAGE, Pages, SongSelectImport
from tests.helpers.songfileimport import SongImportTestHelper
from tests.helpers.testmixin import TestMixin
from tests.utils.constants import RESOURCE_PATH
TEST_PATH = RESOURCE_PATH / 'songs' / 'songselect'
class TestSongSelectImport(TestCase, TestMixin):
"""
Test the :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class
"""
def test_constructor(self):
"""
Test that constructing a basic SongSelectImport object works correctly
"""
# GIVEN: The SongSelectImporter class and a mocked out build_opener
# WHEN: An object is instantiated
importer = SongSelectImport(sentinel.db_manager, sentinel.webview)
# THEN: The object should have the correct properties
assert importer.db_manager is sentinel.db_manager, 'The db_manager should be set'
assert importer.webview is sentinel.webview, 'The webview should be set'
def test_get_page_type_login(self):
"""
Test get_page_type to spot the login page
"""
# GIVEN: A importer object, and a mocked url
importer = SongSelectImport(None, None)
url = QtCore.QUrl('https://profile.ccli.com/account/signin?appContext=SongSelect&'
'returnUrl=https%3a%2f%2fsongselect.ccli.com%2f')
page = MagicMock(url=MagicMock(return_value=url))
importer.webview = MagicMock(page=MagicMock(return_value=page))
# WHEN: The method is run
result = importer.get_page_type()
# THEN: The correct type should be returned
assert result == Pages.Login
def test_get_page_type_home(self):
"""
Test get_page_type to spot the home page
"""
# GIVEN: A importer object, and a mocked url
importer = SongSelectImport(None, None)
url = QtCore.QUrl('https://songselect.ccli.com')
page = MagicMock(url=MagicMock(return_value=url))
importer.webview = MagicMock(page=MagicMock(return_value=page))
# WHEN: The method is run
result = importer.get_page_type()
# THEN: The correct type should be returned
assert result == Pages.Home
def test_get_page_type_search(self):
"""
Test get_page_type to spot the search page
"""
# GIVEN: A importer object, and a mocked url
importer = SongSelectImport(None, None)
url = QtCore.QUrl('https://songselect.ccli.com/search/results?SearchText=test')
page = MagicMock(url=MagicMock(return_value=url))
importer.webview = MagicMock(page=MagicMock(return_value=page))
# WHEN: The method is run
result = importer.get_page_type()
# THEN: The correct type should be returned
assert result == Pages.Search
def test_get_page_type_song(self):
"""
Test get_page_type to spot the login page
"""
# GIVEN: A importer object, and a mocked url
importer = SongSelectImport(None, None)
url = QtCore.QUrl('https://songselect.ccli.com/Songs/7115744/song_name/view_lyrics')
page = MagicMock(url=MagicMock(return_value=url))
importer.webview = MagicMock(page=MagicMock(return_value=page))
# WHEN: The method is run
result = importer.get_page_type()
# THEN: The correct type should be returned
assert result == Pages.Song
def test_get_page_type_other(self):
"""
Test get_page_type to spot the login page
"""
# GIVEN: A importer object, and a mocked url
importer = SongSelectImport(None, None)
url = QtCore.QUrl('https://openlp.org')
page = MagicMock(url=MagicMock(return_value=url))
importer.webview = MagicMock(page=MagicMock(return_value=page))
# WHEN: The method is run
result = importer.get_page_type()
# THEN: The correct type should be returned
assert result == Pages.Other
@patch('openlp.plugins.songs.lib.songselect.wait_for')
def test_run_javascript(self, mocked_wait_for):
"""
Test run javascript calls the page object
"""
# GIVEN: A importer object and mocked run js fn
def runJs(script, handle_result):
handle_result('processed_{}'.format(script))
importer = SongSelectImport(None, None)
importer.webview = MagicMock()
page = MagicMock()
page.runJavaScript = runJs
importer.webview.page.return_value = page
# WHEN: The set login field method is called
result = importer._run_javascript('2 + 2')
# THEN: The javascript should have been called on the page object
assert result == 'processed_2 + 2'
def test_reset_webview(self):
"""
Check that the setUrl method is called when the reset webview method is called
"""
# GIVEN: A importer object and mock webview
importer = SongSelectImport(None, None)
importer.webview = MagicMock()
# WHEN: The reset_webview method is called
importer.reset_webview()
# THEN: The setUrl function should have been called
importer.webview.setUrl.assert_called_with(QtCore.QUrl(LOGIN_PAGE))
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.set_page')
def test_set_home_page(self, mocked_set_page):
"""
Test that when the home method is called, it attempts to go to the home page
"""
# GIVEN: A importer object
importer = SongSelectImport(None, None)
# WHEN: The home method is called
importer.set_home_page()
# THEN: set_page is called once with the base url
mocked_set_page.assert_called_with(BASE_URL)
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport._run_javascript')
def test_set_page(self, mocked_run_js):
"""
Test set page runs the correct script
"""
# GIVEN: A importer object
importer = SongSelectImport(None, None)
# WHEN: The set login field method is called
importer.set_page('my_new_page')
# THEN: The javascript called should contain the correct values
mocked_run_js.assert_called_with('document.location = "my_new_page"')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport._run_javascript')
def test_set_login_fields(self, mocked_run_js):
"""
Test correct js is sent to set login fields
"""
# GIVEN: A importer object
importer = SongSelectImport(None, None)
# WHEN: The set login field method is called
importer.set_login_fields('my_username', 'my_password')
# THEN: The javascript called should contain the correct values
mocked_run_js.assert_called_with(('document.getElementById("EmailAddress").value = "my_username";'
'document.getElementById("Password").value = "my_password";'
))
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport._run_javascript')
@patch('openlp.plugins.songs.lib.songselect.wait_for')
def test_get_page(self, mocked_wait_for, mocked_run_js):
"""
Test get page sends js requests
"""
# GIVEN: A importer object
importer = SongSelectImport(None, None)
mocked_run_js.return_value = True
# WHEN: The get page method is called
importer.get_page("https://example.com")
# THEN: The javascript should be run
assert mocked_run_js.call_count == 2, 'Should be called once for request and once for fetch'
mocked_wait_for.assert_called_once()
def test_get_song_number_from_url(self):
"""
Test the ccli number can be correctly obtained from a url
"""
# GIVEN: A importer object
importer = SongSelectImport(None, None)
# WHEN: The function is called with a valid url
result = importer.get_song_number_from_url('https://songselect.ccli.com/Songs/7115744/way-maker')
# THEN: The ccli number is returned
assert result == '7115744', 'Should have found the ccli number from the url'
def test_get_song_number_from_url_nonumber(self):
"""
Test the ccli number function returns None when no number is found
"""
# GIVEN: A importer object
importer = SongSelectImport(None, None)
# WHEN: The function is called with a valid url
result = importer.get_song_number_from_url('https://songselect.ccli.com/search/results?SearchText=song+7115744')
# THEN: The returned value should be None
assert result is None
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page')
def test_get_song_page_raises_exception(self, mocked_get_page, mock_get_num):
"""
Test that when BeautifulSoup gets a bad song page the get_song() method returns None
"""
# GIVEN: A mocked callback and an importer object
mocked_get_page.side_effect = None
mocked_callback = MagicMock()
importer = SongSelectImport(None, MagicMock())
# WHEN: get_song is called
result = importer.get_song(callback=mocked_callback)
# THEN: The callback should have been called once and None should be returned
mocked_callback.assert_called_with()
assert result is None, 'The get_song() method should have returned None'
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page')
@patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
def test_get_song_lyrics_raise_exception(self, MockedBeautifulSoup, mocked_get_page, mock_get_num):
"""
Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None
"""
# GIVEN: A bunch of mocked out stuff and an importer object
song_page = MagicMock(return_value={'href': '/lyricpage'})
MockedBeautifulSoup.side_effect = [song_page, TypeError('Test Error')]
mocked_callback = MagicMock()
importer = SongSelectImport(None, MagicMock())
# WHEN: get_song is called
result = importer.get_song(callback=mocked_callback)
# THEN: The callback should have been called twice and None should be returned
assert 2 == mocked_callback.call_count, 'The callback should have been called twice'
assert result is None, 'The get_song() method should have returned None'
@patch('openlp.plugins.songs.lib.songselect.log.exception')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page')
def test_get_song_no_access(self, mocked_get_page, mock_get_num, mock_log_exception):
"""
Test that the get_song() handles the case when the user's CCLI account has no access to the song
"""
fake_song_page = '''<!DOCTYPE html><html><body>
<div class="content-title">
<h1>Song Title</h1>
<ul class="authors">
<li><a>Author 1</a></li>
<li><a>Author 2</a></li>
</ul>
</div>
<div class="song-content-data"><ul><li><strong>1234_cclinumber_5678</strong></li></ul></div>
<section class="page-section">
<a title="View song lyrics" data-open="ssUpgradeModal"></a>
</section>
<ul class="song-meta-list">
<li>Themes</li><li><a>theme1</a></li><li><a>theme2</a></li>
</ul>
</body></html>
'''
fake_lyrics_page = '''<!DOCTYPE html><html><body>
<div class="song-viewer lyrics">
<h3>Verse 1</h3>
<p>verse thing 1<br>line 2</p>
<h3>Verse 2</h3>
<p>verse thing 2</p>
</div>
<ul class="copyright">
<li>Copy thing</li><li>Copy thing 2</li>
</ul>
</body></html>
'''
mocked_get_page.side_effect = [fake_song_page, fake_lyrics_page]
mocked_callback = MagicMock()
importer = SongSelectImport(None, MagicMock())
# WHEN: get_song is called
result = importer.get_song(callback=mocked_callback)
# THEN: None should be returned
assert result is None, 'The get_song() method should have returned None'
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page')
def test_get_song(self, mocked_get_page, mock_get_num):
"""
Test that the get_song() method returns the correct song details
"""
fake_song_page = '''<!DOCTYPE html><html><body>
<div class="content-title">
<h1>Song Title</h1>
<ul class="authors">
<li><a>Author 1</a></li>
<li><a>Author 2</a></li>
</ul>
</div>
<div class="song-content-data"><ul><li><strong>1234_cclinumber_5678</strong></li></ul></div>
<section class="page-section">
<a title="View song lyrics" href="pretend link"></a>
</section>
<ul class="song-meta-list">
<li>Themes</li><li><a>theme1</a></li><li><a>theme2</a></li>
</ul>
</body></html>
'''
fake_lyrics_page = '''<!DOCTYPE html><html><body>
<div class="song-viewer lyrics">
<h3>Verse 1</h3>
<p>verse thing 1<br>line 2</p>
<h3>Verse 2</h3>
<p>verse thing 2</p>
<h3>Spoken Words</h3>
<p>completely custom verse type</p>
</div>
<ul class="copyright">
<li>Copy thing</li><li>Copy thing 2</li>
</ul>
</body></html>
'''
mocked_get_page.side_effect = [fake_song_page, fake_lyrics_page]
mocked_callback = MagicMock()
importer = SongSelectImport(None, MagicMock())
# WHEN: get_song is called
result = importer.get_song(callback=mocked_callback)
# THEN: The callback should have been called three times and the song should be returned
assert 3 == mocked_callback.call_count, 'The callback should have been called twice'
assert result is not None, 'The get_song() method should have returned a song dictionary'
assert result['title'] == 'Song Title'
assert result['authors'] == ['Author 1', 'Author 2']
assert result['copyright'] == 'Copy thing/Copy thing 2'
assert result['topics'] == ['theme1', 'theme2']
assert result['ccli_number'] == '1234_cclinumber_5678'
assert result['verses'] == [{'label': 'Verse 1', 'lyrics': 'verse thing 1\nline 2'},
{'label': 'Verse 2', 'lyrics': 'verse thing 2'},
{'label': 'Spoken Words', 'lyrics': 'completely custom verse type'}]
@patch('openlp.plugins.songs.lib.songselect.clean_song')
@patch('openlp.plugins.songs.lib.songselect.Topic')
@patch('openlp.plugins.songs.lib.songselect.Author')
def test_save_song_new_author(self, MockedAuthor, MockedTopic, mocked_clean_song):
"""
Test that saving a song with a new author performs the correct actions
"""
# GIVEN: A song to save, and some mocked out objects
song_dict = {
'title': 'Arky Arky',
'authors': ['Public Domain'],
'verses': [
{'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'},
{'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'},
{'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'}
],
'copyright': 'Public Domain',
'ccli_number': '123456'
}
MockedAuthor.display_name.__eq__.return_value = False
MockedTopic.name.__eq__.return_value = False
mocked_db_manager = MagicMock()
mocked_db_manager.get_object_filtered.return_value = None
importer = SongSelectImport(mocked_db_manager, MagicMock())
# WHEN: The song is saved to the database
result = importer.save_song(song_dict)
# THEN: The return value should be a Song class and the mocked_db_manager should have been called
assert isinstance(result, Song), 'The returned value should be a Song object'
mocked_clean_song.assert_called_with(mocked_db_manager, result)
assert 2 == mocked_db_manager.save_object.call_count, \
'The save_object() method should have been called twice'
mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
MockedAuthor.assert_called_with(first_name='Public', last_name='Domain', display_name='Public Domain')
assert 1 == len(result.authors_songs), 'There should only be one author'
@patch('openlp.plugins.songs.lib.songselect.clean_song')
@patch('openlp.plugins.songs.lib.songselect.Author')
def test_save_song_existing_author(self, MockedAuthor, mocked_clean_song):
"""
Test that saving a song with an existing author performs the correct actions
"""
# GIVEN: A song to save, and some mocked out objects
song_dict = {
'title': 'Arky Arky',
'authors': ['Public Domain'],
'verses': [
{'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'},
{'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'},
{'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'}
],
'copyright': 'Public Domain',
'ccli_number': '123456'
}
MockedAuthor.display_name.__eq__.return_value = False
mocked_db_manager = MagicMock()
mocked_db_manager.get_object_filtered.return_value = MagicMock()
importer = SongSelectImport(mocked_db_manager, MagicMock())
# WHEN: The song is saved to the database
result = importer.save_song(song_dict)
# THEN: The return value should be a Song class and the mocked_db_manager should have been called
assert isinstance(result, Song), 'The returned value should be a Song object'
mocked_clean_song.assert_called_with(mocked_db_manager, result)
assert 2 == mocked_db_manager.save_object.call_count, \
'The save_object() method should have been called twice'
mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
assert 0 == MockedAuthor.call_count, 'A new author should not have been instantiated'
assert 1 == len(result.authors_songs), 'There should only be one author'
@patch('openlp.plugins.songs.lib.songselect.clean_song')
@patch('openlp.plugins.songs.lib.songselect.Author')
def test_save_song_unknown_author(self, MockedAuthor, mocked_clean_song):
"""
Test that saving a song with an author name of only one word performs the correct actions
"""
# GIVEN: A song to save, and some mocked out objects
song_dict = {
'title': 'Arky Arky',
'authors': ['Unknown'],
'verses': [
{'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'},
{'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'},
{'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'}
],
'copyright': 'Public Domain',
'ccli_number': '123456'
}
MockedAuthor.display_name.__eq__.return_value = False
mocked_db_manager = MagicMock()
mocked_db_manager.get_object_filtered.return_value = None
importer = SongSelectImport(mocked_db_manager, MagicMock())
# WHEN: The song is saved to the database
result = importer.save_song(song_dict)
# THEN: The return value should be a Song class and the mocked_db_manager should have been called
assert isinstance(result, Song), 'The returned value should be a Song object'
mocked_clean_song.assert_called_with(mocked_db_manager, result)
assert 2 == mocked_db_manager.save_object.call_count, \
'The save_object() method should have been called twice'
mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
MockedAuthor.assert_called_with(first_name='Unknown', last_name='', display_name='Unknown')
assert 1 == len(result.authors_songs), 'There should only be one author'
@patch('openlp.plugins.songs.lib.songselect.clean_song')
@patch('openlp.plugins.songs.lib.songselect.Topic')
@patch('openlp.plugins.songs.lib.songselect.Author')
def test_save_song_topics(self, MockedAuthor, MockedTopic, mocked_clean_song):
"""
Test that saving a song with topics performs the correct actions
Also check that a verse with no number is retitled to 1
"""
# GIVEN: A song to save, and some mocked out objects
song_dict = {
'title': 'Arky Arky',
'authors': ['Public Domain'],
'verses': [
{'label': 'Verse', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'},
{'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'},
{'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'}
],
'copyright': 'Public Domain',
'ccli_number': '123456',
'topics': ['Old Testement', 'Flood']
}
def save_object(b):
b.topics = []
MockedAuthor.display_name.__eq__.return_value = False
MockedTopic.name.__eq__.return_value = False
mocked_db_manager = MagicMock()
mocked_db_manager.get_object_filtered.return_value = None
mocked_db_manager.save_object = save_object
importer = SongSelectImport(mocked_db_manager, MagicMock())
# WHEN: The song is saved to the database
result = importer.save_song(song_dict)
# THEN: The return value should be a Song class and the topics should have been added
assert isinstance(result, Song), 'The returned value should be a Song object'
mocked_clean_song.assert_called_with(mocked_db_manager, result)
assert MockedTopic.call_count == 2, 'Should have created 2 new topics'
MockedTopic.assert_called_with(name='Flood')
assert 1 == len(result.authors_songs), 'There should only be one author'
class TestSongSelectForm(TestCase, TestMixin):
"""
Test the :class:`~openlp.plugins.songs.forms.songselectform.SongSelectForm` class
"""
def setUp(self):
"""
Some set up for this test suite
"""
self.setup_application()
self.app.setApplicationVersion('0.0')
self.app.process_events = lambda: None
Registry.create()
Registry().register('application', self.app)
Registry().register('settings', MagicMock())
self.grid_patcher = patch('openlp.plugins.songs.forms.songselectdialog.QtWidgets.QGridLayout')
self.web_patcher = patch('openlp.plugins.songs.forms.songselectdialog.WebEngineView')
self.vbox_patcher = patch('openlp.plugins.songs.forms.songselectdialog.QtWidgets.QVBoxLayout')
self.grid_patcher.start()
self.web_patcher.start()
self.vbox_patcher.start()
def tearDown(self):
self.grid_patcher.stop()
self.web_patcher.stop()
self.vbox_patcher.stop()
def test_create_form(self):
"""
Test that we can create the SongSelect form
"""
# GIVEN: The SongSelectForm class and a mocked db manager
mocked_plugin = MagicMock()
mocked_db_manager = MagicMock()
# WHEN: We create an instance
ssform = SongSelectForm(None, mocked_plugin, mocked_db_manager)
# THEN: The correct properties should have been assigned
assert mocked_plugin == ssform.plugin, 'The correct plugin should have been assigned'
assert mocked_db_manager == ssform.db_manager, 'The correct db_manager should have been assigned'
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport')
def test_initialise(self, mocked_ss_import):
"""
Test the initialise method
"""
# GIVEN: The SongSelectForm
ssform = SongSelectForm(None, MagicMock(), MagicMock())
# WHEN: The initialise method is run
ssform.initialise()
# THEN: The import object should exist, song var should be None, and the page hooked up
assert ssform.song is None
assert isinstance(ssform.song_select_importer, SongSelectImport), 'SongSelectImport object should be created'
assert ssform.webview.page.call_count == 2, 'Page should be called twice, once for each load handler'
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QDialog.exec')
def test_exec(self, mocked_exec):
"""
Test the exec method
"""
# GIVEN: The SongSelectForm
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_select_importer = MagicMock()
ssform.stacked_widget = MagicMock()
# WHEN: The initialise method is run
ssform.exec()
# THEN: Should have reset webview, set stack to 0 and pass on the event
ssform.song_select_importer.reset_webview.assert_called_once()
ssform.stacked_widget.setCurrentIndex.assert_called_with(0)
mocked_exec.assert_called_once()
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QDialog.done')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QProgressDialog')
def test_done(self, mocked_prog_dialog, mocked_done):
"""
Test the done method closes th dialog
"""
# GIVEN: The SongSelectForm
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_select_importer = MagicMock()
# WHEN: The initialise method is run
ssform.done('result')
# THEN: Should have passed on the event
mocked_done.assert_called_once()
def test_page_load_started(self):
"""
Test the page_load_started method
"""
# GIVEN: The SongSelectForm
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_progress_bar = MagicMock()
ssform.import_button = MagicMock()
ssform.view_button = MagicMock()
ssform.back_button = MagicMock()
ssform.url_bar = MagicMock()
ssform.message_area = MagicMock()
# WHEN: The method is run
ssform.page_load_started()
# THEN: The UI should be set up accordingly (working bar and disabled buttons)
ssform.song_progress_bar.setMaximum.assert_called_with(0)
ssform.song_progress_bar.setVisible.assert_called_with(True)
ssform.import_button.setEnabled.assert_called_with(False)
ssform.view_button.setEnabled.assert_called_with(False)
ssform.back_button.setEnabled.assert_called_with(False)
ssform.message_area.setText.assert_called_with('')
def test_page_loaded_login(self):
"""
Test the page_loaded method for a "Login" page
"""
# GIVEN: The SongSelectForm and mocked login page
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_select_importer = MagicMock()
ssform.song_select_importer.get_page_type.return_value = Pages.Login
ssform.signin_page_loaded = MagicMock()
ssform.url_bar = MagicMock()
# WHEN: The method is run
ssform.page_loaded(True)
# THEN: The signin page method should be called
ssform.signin_page_loaded.assert_called_once()
def test_page_loaded_song(self):
"""
Test the page_loaded method for a "Song" page
"""
# GIVEN: The SongSelectForm and mocked song page
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_select_importer = MagicMock()
ssform.song_select_importer.get_page_type.return_value = Pages.Song
ssform.song_progress_bar = MagicMock()
ssform.url_bar = MagicMock()
# WHEN: The method is run
ssform.page_loaded(True)
# THEN: Progress bar should have been set max 3 (for loading song)
ssform.song_progress_bar.setMaximum.assert_called_with(3)
ssform.song_progress_bar.setVisible.call_count == 2
@patch('openlp.plugins.songs.forms.songselectform.translate')
def test_page_loaded_song_no_access(self, mocked_translate):
"""
Test the page_loaded method for a "Song" page to which the CCLI account has no access
"""
# GIVEN: The SongSelectForm and mocked song page and translate function
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_select_importer = MagicMock()
ssform.song_select_importer.get_page_type.return_value = Pages.Song
ssform.song_select_importer.get_song.return_value = None
ssform.song_progress_bar = MagicMock()
ssform.url_bar = MagicMock()
ssform.message_area = MagicMock()
mocked_translate.return_value = 'some message'
# WHEN: The method is run
ssform.page_loaded(True)
# THEN: The no access message should be shown and the progress bar should be less than 3
ssform.message_area.setText.assert_called_with('some message')
ssform.song_progress_bar.setValue.call_count < 4
def test_page_loaded_other(self):
"""
Test the page_loaded method for an "Other" page
"""
# GIVEN: The SongSelectForm and mocked other page
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_select_importer = MagicMock()
ssform.song_select_importer.get_page_type.return_value = Pages.Other
ssform.song_progress_bar = MagicMock()
ssform.back_button = MagicMock()
ssform.url_bar = MagicMock()
# WHEN: The method is run
ssform.page_loaded(True)
# THEN: Back button should be available
ssform.back_button.setEnabled.assert_called_with(True)
def test_signin_page_loaded(self):
"""
Test that the signin_page_loaded method calls the appropriate method
"""
# GIVEN: The SongSelectForm and mocked settings
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_select_importer = MagicMock()
ssform.settings.value = MagicMock(side_effect=['user', 'pass'])
# WHEN: The method is run
ssform.signin_page_loaded()
# THEN: Correct values should have been sent from the settings
ssform.song_select_importer.set_login_fields.assert_called_with('user', 'pass')
@patch('openlp.plugins.songs.forms.songselectdialog.QtWidgets.QListWidgetItem')
def test_view_song(self, mock_qtlist):
"""
Test that the _view_song method does the important stuff
"""
# GIVEN: The SongSelectForm, mocks and a song
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.stacked_widget = MagicMock()
ssform.title_edit = MagicMock()
ssform.copyright_edit = MagicMock()
ssform.ccli_edit = MagicMock()
ssform.lyrics_table_widget = MagicMock()
ssform.author_list_widget = MagicMock()
ssform.song = {
'title': 'Song Title',
'copyright': 'copy thing',
'ccli_number': '1234',
'authors': ['Bob', 'Jo'],
'verses': [{'lyrics': 'hello', 'label': 'Verse 1'}]
}
# WHEN: The method is run
ssform._view_song()
# THEN: Page should have changed in the stacked widget and ui should have been updated
ssform.stacked_widget.setCurrentIndex.assert_called_with(1)
ssform.title_edit.setText.assert_called_with('Song Title')
ssform.copyright_edit.setText.assert_called_with('copy thing')
ssform.ccli_edit.setText.assert_called_with('1234')
assert ssform.lyrics_table_widget.setItem.call_count > 0
assert ssform.author_list_widget.addItem.call_count > 0
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.critical')
def test_view_song_invalid(self, mock_message):
"""
Test that the _view_song doesn't mess up when the song doesn't exist
"""
# GIVEN: The SongSelectForm, mocks and a song
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.stacked_widget = MagicMock()
ssform.song = None
# WHEN: The method is run
ssform._view_song()
# THEN: Page should not have changed and a warning should show
assert ssform.stacked_widget.setCurrentIndex.call_count == 0
mock_message.assert_called_once()
def test_on_url_bar_return_pressed(self):
"""
Test that the on_url_bar_return_pressed method changes the page
"""
# GIVEN: The SongSelectForm, mocks and a song
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.url_bar = MagicMock()
ssform.url_bar.text.return_value = "test"
ssform.song_select_importer = MagicMock()
# WHEN: The method is run
ssform.on_url_bar_return_pressed()
# THEN: Page should not have changed and a warning should show
ssform.song_select_importer.set_page.assert_called_with("test")
@patch('openlp.plugins.songs.forms.songselectform.and_')
@patch('openlp.plugins.songs.forms.songselectform.Song')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.information')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question')
@patch('openlp.plugins.songs.forms.songselectform.translate')
def test_on_import(self, mocked_trans, mocked_quest, mocked_info, mocked_song, mocked_and):
"""
Test that when a song is imported and the user clicks the "yes" button, the UI goes back to the previous page
"""
# GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method
mocked_trans.side_effect = lambda *args: args[1]
mocked_quest.return_value = QtWidgets.QMessageBox.Yes
ssform = SongSelectForm(None, MagicMock(), MagicMock())
mocked_song_select_importer = MagicMock()
ssform.song_select_importer = mocked_song_select_importer
ssform.song = {'ccli_number': '1234'}
# WHEN: The import button is clicked, and the user clicks Yes
with patch.object(ssform, 'on_back_button_clicked') as mocked_on_back_button_clicked:
ssform.on_import_button_clicked()
# THEN: The on_back_button_clicked() method should have been called
mocked_song_select_importer.save_song.assert_called_with({'ccli_number': '1234'})
mocked_quest.assert_not_called()
mocked_info.assert_called_once()
mocked_on_back_button_clicked.assert_called_with(True)
assert ssform.song is None
@patch('openlp.plugins.songs.forms.songselectform.len')
@patch('openlp.plugins.songs.forms.songselectform.and_')
@patch('openlp.plugins.songs.forms.songselectform.Song')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.information')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question')
@patch('openlp.plugins.songs.forms.songselectform.translate')
def test_on_import_duplicate_yes_clicked(self, mock_trans, mock_q, mocked_info, mock_song, mock_and, mock_len):
"""
Test that when a duplicate song is imported and the user clicks the "yes" button, the song is imported
"""
# GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method
mock_len.return_value = 1
mock_trans.side_effect = lambda *args: args[1]
mock_q.return_value = QtWidgets.QMessageBox.Yes
ssform = SongSelectForm(None, MagicMock(), MagicMock())
mocked_song_select_importer = MagicMock()
ssform.song_select_importer = mocked_song_select_importer
ssform.song = {'ccli_number': '1234'}
# WHEN: The import button is clicked, and the user clicks Yes
with patch.object(ssform, 'on_back_button_clicked') as mocked_on_back_button_clicked:
ssform.on_import_button_clicked()
# THEN: Should have been saved and the on_back_button_clicked() method should have been called
mocked_song_select_importer.save_song.assert_called_with({'ccli_number': '1234'})
mock_q.assert_called_once()
mocked_info.assert_called_once()
mocked_on_back_button_clicked.assert_called_once()
assert ssform.song is None
@patch('openlp.plugins.songs.forms.songselectform.len')
@patch('openlp.plugins.songs.forms.songselectform.and_')
@patch('openlp.plugins.songs.forms.songselectform.Song')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.information')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question')
@patch('openlp.plugins.songs.forms.songselectform.translate')
def test_on_import_duplicate_no_clicked(self, mock_trans, mock_q, mocked_info, mock_song, mock_and, mock_len):
"""
Test that when a duplicate song is imported and the user clicks the "no" button, the UI exits
"""
# GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method
mock_len.return_value = 1
mock_trans.side_effect = lambda *args: args[1]
mock_q.return_value = QtWidgets.QMessageBox.No
ssform = SongSelectForm(None, MagicMock(), MagicMock())
mocked_song_select_importer = MagicMock()
ssform.song_select_importer = mocked_song_select_importer
ssform.song = {'ccli_number': '1234'}
# WHEN: The import button is clicked, and the user clicks No
with patch.object(ssform, 'on_back_button_clicked') as mocked_on_back_button_clicked:
ssform.on_import_button_clicked()
# THEN: Should have not been saved
assert mocked_song_select_importer.save_song.call_count == 0
mock_q.assert_called_once()
mocked_info.assert_not_called()
mocked_on_back_button_clicked.assert_not_called()
assert ssform.song is not None
def test_on_back_button_clicked_preview(self):
"""
Test that when the back button is clicked on preview screen, the stacked widget is set back one page
"""
# GIVEN: A SongSelect form, stacked widget on page 1
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssimporter = MagicMock()
ssform.song_select_importer = MagicMock()
ssform.song_select_importer.set_home_page = ssimporter
with patch.object(ssform, 'stacked_widget') as mocked_stacked_widget:
mocked_stacked_widget.currentIndex.return_value = 1
# WHEN: The preview back button is clicked
ssform.on_back_button_clicked()
# THEN: The stacked widget should be set back one page and webpage is NOT put back to the home page
mocked_stacked_widget.setCurrentIndex.assert_called_with(0)
ssimporter.assert_not_called()
def test_on_back_button_clicked_force(self):
"""
Test that when the back button method is triggered with the force param set,
the page should be changed
"""
# GIVEN: A SongSelect form, stacked widget on page 1
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssimporter = MagicMock()
ssform.song_select_importer = MagicMock()
ssform.song_select_importer.set_home_page = ssimporter
with patch.object(ssform, 'stacked_widget') as mocked_stacked_widget:
mocked_stacked_widget.currentIndex.return_value = 1
# WHEN: The preview back button is clicked with force param
ssform.on_back_button_clicked(True)
# THEN: The stacked widget should be set back one page and webpage is NOT put back to the home page
mocked_stacked_widget.setCurrentIndex.assert_called_with(0)
ssimporter.assert_called_once()
def test_on_back_button_clicked(self):
"""
Test that when the back button is clicked, the stacked widget is set to page 0
and set to home page
"""
# GIVEN: A SongSelect form, stacked widget on page 0
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssimporter = MagicMock()
ssform.song_select_importer = MagicMock()
ssform.song_select_importer.set_home_page = ssimporter
with patch.object(ssform, 'stacked_widget') as mocked_stacked_widget:
mocked_stacked_widget.currentIndex.return_value = 0
# WHEN: The back button is clicked
ssform.on_back_button_clicked()
# THEN: The stacked widget should be set back one page
mocked_stacked_widget.setCurrentIndex.assert_called_with(0)
ssimporter.assert_called_with()
def test_update_song_progress(self):
"""
Test the _update_song_progress() method
"""
# GIVEN: A SongSelect form
ssform = SongSelectForm(None, MagicMock(), MagicMock())
# WHEN: _update_song_progress() is called
with patch.object(ssform, 'song_progress_bar') as mocked_song_progress_bar:
mocked_song_progress_bar.value.return_value = 2
ssform._update_song_progress()
# THEN: The song progress bar should be updated
mocked_song_progress_bar.setValue.assert_called_with(3)
def test_on_view_button_clicked(self):
"""
Test that view song function is run when the view button is clicked
"""
# GIVEN: A SongSelect form
ssform = SongSelectForm(None, MagicMock(), MagicMock())
# WHEN: A song result is double-clicked
with patch.object(ssform, '_view_song') as mocked_view_song:
ssform.on_view_button_clicked()
# THEN: The song is fetched and shown to the user
mocked_view_song.assert_called_with()
def test_songselect_file_import():
"""
Test that loading a SongSelect file works correctly on various files
"""
with SongImportTestHelper('CCLIFileImport', 'cclifile') as helper:
helper.file_import([TEST_PATH / 'TestSong.bin'],
helper.load_external_result_data(TEST_PATH / 'TestSong-bin.json'))
helper.file_import([TEST_PATH / 'TestSong.txt'],
helper.load_external_result_data(TEST_PATH / 'TestSong-txt.json'))