# -*- coding: utf-8 -*- ########################################################################## # OpenLP - Open Source Lyrics Projection # # ---------------------------------------------------------------------- # # Copyright (c) 2008-2022 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 . # ########################################################################## """ Package to test the openlp.core.ui.firsttimeform package. """ import pytest from pathlib import Path from unittest.mock import MagicMock, call, patch, DEFAULT from PyQt5 import QtCore, QtWidgets, QtTest from openlp.core.common.registry import Registry from openlp.core.ui.firsttimeform import FirstTimeForm, ThemeListWidgetItem from openlp.core.ui.firsttimewizard import RemotePage, ThemeListWidget from openlp.core.ui.icons import UiIcons INVALID_CONFIG = """ { "_comments": "The most recent version should be added to https://openlp.org/files/frw/download_3.0.json", "_meta": { } """ sample_theme_data = {'file_name': 'BlueBurst.otz', 'sha256': 'sha_256_hash', 'thumbnail': 'BlueBurst.png', 'title': 'Blue Burst'} @pytest.fixture() def ftf_app(registry, qapp): Registry().register('application', qapp) @pytest.fixture() def download_env(registry): download_worker_patcher = patch('openlp.core.ui.firsttimeform.DownloadWorker') run_thread_patcher = patch('openlp.core.ui.firsttimeform.run_thread') mocked_download_worker = download_worker_patcher.start() mocked_run_thread = run_thread_patcher.start() yield mocked_download_worker, mocked_run_thread download_worker_patcher.stop() run_thread_patcher.stop() @pytest.fixture() def mocked_set_icon(mock_settings): move_to_thread_patcher = patch('openlp.core.ui.firsttimeform.DownloadWorker.moveToThread').start() set_icon_patcher = patch('openlp.core.ui.firsttimeform.ThemeListWidgetItem.setIcon').start() q_thread_patcher = patch('openlp.core.ui.firsttimeform.QtCore.QThread').start() mocked_app = MagicMock() mocked_app.worker_threads = {} mocked_main_window = MagicMock() Registry().remove('application') Registry().register('application', mocked_app) Registry().register('main_window', mocked_main_window) yield set_icon_patcher move_to_thread_patcher.stop() set_icon_patcher.stop() q_thread_patcher.stop() def test_init_sample_data(download_env): """ Test that the theme data is loaded correctly in to a ThemeListWidgetItem object when instantiated """ # GIVEN: A sample theme dictionary object # WHEN: Creating an instance of `ThemeListWidgetItem` mocked_download_worker = download_env[0] instance = ThemeListWidgetItem('url', sample_theme_data, MagicMock()) # THEN: The data should have been set correctly assert instance.file_name == 'BlueBurst.otz' assert instance.sha256 == 'sha_256_hash' assert instance.text() == 'Blue Burst' assert instance.toolTip() == 'Blue Burst' mocked_download_worker.assert_called_once_with('url', 'BlueBurst.png') def test_init_download_worker(download_env): """ Test that the `DownloadWorker` worker is set up correctly and that the thread is started. """ # GIVEN: A sample theme dictionary object mocked_download_worker = download_env[0] mocked_run_thread = download_env[1] mocked_ftw = MagicMock(spec=FirstTimeForm) mocked_ftw.thumbnail_download_threads = [] # WHEN: Creating an instance of `ThemeListWidgetItem` instance = ThemeListWidgetItem('url', sample_theme_data, mocked_ftw) # THEN: The `DownloadWorker` should have been set up with the appropriate data mocked_download_worker.assert_called_once_with('url', 'BlueBurst.png') mocked_download_worker.download_failed.connect.called_once_with(instance._on_download_failed()) mocked_download_worker.download_succeeded.connect.called_once_with(instance._on_thumbnail_downloaded) mocked_run_thread.assert_called_once_with(mocked_download_worker(), 'thumbnail_download_BlueBurst.png') assert mocked_ftw.thumbnail_download_threads == ['thumbnail_download_BlueBurst.png'] def test_firsttimeform_initialise(): """ Test if we can intialise the FirstTimeForm """ # GIVEN: A First Time Wizard and an expected screen object frw = FirstTimeForm(None) expected_screens = MagicMock() # WHEN: The First Time Wizard is initialised frw.initialize(expected_screens) # THEN: The screens should be set up, and the default values initialised assert expected_screens == frw.screens, 'The screens should be correct' assert frw.has_web_access is True, 'The default value of self.web_access should be True' assert [] == frw.thumbnail_download_threads, 'The list of threads should be empty' assert frw.has_run_wizard is False, 'has_run_wizard should be False' @patch('openlp.core.ui.firsttimeform.QtWidgets.QWizard.exec') def test_firsttimeform_exec(mocked_qwizard_exec): # GIVEN: An instance of FirstTimeForm frw = FirstTimeForm(None) with patch.object(frw, 'set_defaults') as mocked_set_defaults: # WHEN: exec is called frw.exec() # THEN: The wizard should be reset and the exec methon on the super class should have been called mocked_set_defaults.assert_called_once() mocked_qwizard_exec.assert_called_once() def test_set_defaults(mock_settings): """ Test that the default values are set when set_defaults() is run """ # GIVEN: An initialised FRW and a whole lot of stuff mocked out frw = FirstTimeForm(None) frw.initialize(MagicMock()) mocked_settings = MagicMock() mocked_settings.value.side_effect = lambda key: {'core/has run wizard': False}[key] with patch.object(frw, 'restart') as mocked_restart, \ patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \ patch.object(frw, 'theme_combo_box') as mocked_theme_combo_box, \ patch.object(frw, 'songs_check_box') as mocked_songs_check_box, \ patch.object(Registry, 'register_function') as mocked_register_function, \ patch('openlp.core.ui.firsttimeform.gettempdir', return_value='temp') as mocked_gettempdir, \ patch('openlp.core.ui.firsttimeform.create_paths') as mocked_create_paths, \ patch.object(frw.application, 'set_normal_cursor'): mocked_theme_manager = MagicMock() Registry().register('theme_manager', mocked_theme_manager) Registry().remove('settings') Registry().register('settings', mocked_settings) # WHEN: The set_defaults() method is run frw.set_defaults() # THEN: The default values should have been set mocked_restart.assert_called_once() assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set' mocked_currentIdChanged.connect.assert_called_once_with(frw.on_current_id_changed) mocked_register_function.assert_called_once_with('config_screen_changed', frw.screen_selection_widget.load) mocked_settings.value.assert_has_calls([call('core/has run wizard')]) mocked_gettempdir.assert_called_once() mocked_create_paths.assert_called_once_with(Path('temp', 'openlp')) mocked_theme_combo_box.clear.assert_called_once() mocked_theme_manager.assert_not_called() mocked_songs_check_box.assert_not_called() @patch('openlp.core.ui.firsttimeform.QtWidgets.QWizard.accept') def test_accept_method(mocked_qwizard_accept, registry, *args): """ Test the FirstTimeForm.accept method """ # GIVEN: An instance of FirstTimeForm frw = FirstTimeForm(None) with patch.object(frw, '_set_plugin_status') as mocked_set_plugin_status, \ patch.multiple(frw, songs_check_box=DEFAULT, bible_check_box=DEFAULT, presentation_check_box=DEFAULT, image_check_box=DEFAULT, media_check_box=DEFAULT, custom_check_box=DEFAULT, song_usage_check_box=DEFAULT, alert_check_box=DEFAULT) as mocked_check_boxes, \ patch.object(frw, 'screen_selection_widget') as mocked_screen_selection_widget: # WHEN: Calling accept frw.accept() # THEN: The selected plugins should be enabled, the screen selection saved and the super method called mocked_set_plugin_status.assert_has_calls([ call(mocked_check_boxes['songs_check_box'], 'songs/status'), call(mocked_check_boxes['bible_check_box'], 'bibles/status'), call(mocked_check_boxes['presentation_check_box'], 'presentations/status'), call(mocked_check_boxes['image_check_box'], 'images/status'), call(mocked_check_boxes['media_check_box'], 'media/status'), call(mocked_check_boxes['custom_check_box'], 'custom/status'), call(mocked_check_boxes['song_usage_check_box'], 'songusage/status'), call(mocked_check_boxes['alert_check_box'], 'alerts/status')]) mocked_screen_selection_widget.save.assert_called_once() mocked_qwizard_accept.assert_called_once() def test_accept_method_theme_not_selected(mock_settings): """ Test the FirstTimeForm.accept method when there is no default theme selected """ # GIVEN: An instance of FirstTimeForm mock_settings = Registry().get('settings') frw = FirstTimeForm(None) with patch.object(frw, '_set_plugin_status'), patch.object(frw, 'screen_selection_widget'), \ patch.object(frw, 'theme_combo_box', **{'currentIndex.return_value': -1}): # WHEN: Calling accept and the currentIndex method of the theme_combo_box returns -1 frw.accept() # THEN: OpenLP should not try to save a theme name mock_settings().setValue.assert_not_called() @patch('openlp.core.ui.firsttimeform.QtWidgets.QWizard.reject') @patch('openlp.core.ui.firsttimeform.time') @patch('openlp.core.ui.firsttimeform.get_thread_worker') @patch('openlp.core.ui.firsttimeform.is_thread_finished') def test_reject_method(mocked_is_thread_finished, mocked_get_thread_worker, mocked_time, mocked_qwizard_reject, ftf_app): """ Test that the reject method shuts down the threads correctly """ # GIVEN: A FRW, some mocked threads and workers (that isn't quite done) and other mocked stuff mocked_worker = MagicMock() mocked_get_thread_worker.return_value = mocked_worker mocked_is_thread_finished.side_effect = [False, True] frw = FirstTimeForm(None) frw.initialize(MagicMock()) frw.thumbnail_download_threads = ['test_thread'] with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor: # WHEN: the reject method is called frw.reject() # THEN: The right things should be called in the right order mocked_get_thread_worker.assert_called_once_with('test_thread') mocked_worker.cancel_download.assert_called_once() mocked_is_thread_finished.assert_called_with('test_thread') assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice' mocked_time.sleep.assert_called_once_with(0.1) mocked_set_normal_cursor.assert_called_once() mocked_qwizard_reject.assert_called_once() @patch('openlp.core.ui.firsttimeform.ProxyDialog') def test_on_custom_button_clicked(mocked_proxy_dialog): """ Test _on_custom_button when it is called whe the 'internet settings' (CustomButton1) button is not clicked. """ # GIVEN: An instance of the FirstTimeForm frw = FirstTimeForm(None) # WHEN: Calling _on_custom_button_clicked with a different button to the 'internet settings button. frw._on_custom_button_clicked(QtWidgets.QWizard.CustomButton2) # THEN: The ProxyDialog should not be shown. mocked_proxy_dialog.assert_not_called() @patch('openlp.core.ui.firsttimeform.ProxyDialog') def test_on_custom_button_clicked_internet_settings(mocked_proxy_dialog): """ Test _on_custom_button when it is called when the 'internet settings' (CustomButton1) button is clicked. """ # GIVEN: An instance of the FirstTimeForm frw = FirstTimeForm(None) # WHEN: Calling _on_custom_button_clicked with the constant for the 'internet settings' button (CustomButton1) frw._on_custom_button_clicked(QtWidgets.QWizard.CustomButton1) # THEN: The ProxyDialog should be shown. mocked_proxy_dialog.assert_called_with(frw) mocked_proxy_dialog().retranslate_ui.assert_called_once() mocked_proxy_dialog().exec.assert_called_once() @patch('openlp.core.ui.firsttimeform.QtWidgets.QMessageBox') def test__parse_config_invalid_config(mocked_message_box): """ Test `FirstTimeForm._parse_config` when called with invalid data """ # GIVEN: An instance of `FirstTimeForm` first_time_form = FirstTimeForm(None) # WHEN: Calling _parse_config with a string containing invalid data result = first_time_form._parse_config(INVALID_CONFIG) # THEN: _parse_data should return False and the user should have should have been informed. assert result is False mocked_message_box.critical.assert_called_once_with( first_time_form, 'Invalid index file', 'OpenLP was unable to read the resource index file. Please try again later.') @patch('openlp.core.ui.firsttimeform.get_web_page') @patch('openlp.core.ui.firsttimeform.QtWidgets.QMessageBox') def test_network_error(mocked_message_box, mocked_get_web_page, ftf_app): """ Test we catch a network error in First Time Wizard - bug 1409627 """ # GIVEN: Initial setup and mocks first_time_form = FirstTimeForm(None) first_time_form.initialize(MagicMock()) mocked_get_web_page.side_effect = ConnectionError('') mocked_message_box.Ok = 'OK' # WHEN: the First Time Wizard calls to get the initial configuration first_time_form._download_index() # THEN: the critical_error_message_box should have been called mocked_message_box.critical.assert_called_once_with( first_time_form, 'Network Error', 'There was a network error attempting to connect to retrieve initial configuration information', 'OK') def test_accept_method_theme_selected(mock_settings): """ Test the FirstTimeForm.accept method when a default theme is selected """ # GIVEN: An instance of FirstTimeForm frw = FirstTimeForm(None) mock_settings.value.return_value = True with patch.object(frw, '_set_plugin_status'), \ patch.object(frw, 'screen_selection_widget'), \ patch.object( frw, 'theme_combo_box', **{'currentIndex.return_value': 0, 'currentText.return_value': 'Test Item'}): # WHEN: Calling accept and the currentIndex method of the theme_combo_box returns 0 frw.accept() # THEN: The 'currentItem' in the combobox should have been set as the default theme. mock_settings.setValue.assert_called_once_with('themes/global theme', 'Test Item') def test_on_projectors_check_box_checked(mock_settings): """ Test that the projector panel is shown when the checkbox in the first time wizard is checked """ # GIVEN: A First Time Wizard and a mocked settings object frw = FirstTimeForm(None) mock_settings.value.return_value = True # WHEN: on_projectors_check_box_clicked() is called frw.on_projectors_check_box_clicked() # THEN: The visibility of the projects panel should have been set mock_settings.value.assert_called_once_with('projector/show after wizard') mock_settings.setValue.assert_called_once_with('projector/show after wizard', False) def test_on_projectors_check_box_unchecked(mock_settings): """ Test that the projector panel is shown when the checkbox in the first time wizard is checked """ # GIVEN: A First Time Wizard and a mocked settings object frw = FirstTimeForm(None) mock_settings.value.return_value = False # WHEN: on_projectors_check_box_clicked() is called frw.on_projectors_check_box_clicked() # THEN: The visibility of the projects panel should have been set mock_settings.value.assert_called_once_with('projector/show after wizard') mock_settings.setValue.assert_called_once_with('projector/show after wizard', True) def test_remote_page_get_can_download_remote(ftf_app): """ Test that the `can_download_remote` property returns the correct value """ # GIVEN: A RemotePage object with a mocked out download_checkbox remote_page = RemotePage(None) remote_page.download_checkbox = MagicMock(**{"isChecked.return_value": True}) # WHEN: The can_download_remote property is accessed result = remote_page.can_download_remote # THEN: The result should be True assert result is True def test_remote_page_set_can_download_remote(ftf_app): """ Test that the `can_download_remote` property sets the correct value """ # GIVEN: A RemotePage object with a mocked out download_checkbox remote_page = RemotePage(None) remote_page.download_checkbox = MagicMock() # WHEN: The can_download_remote property is set remote_page.can_download_remote = False # THEN: The result should be True remote_page.download_checkbox.setChecked.assert_called_once_with(False) def test_remote_page_set_can_download_remote_not_bool(ftf_app): """ Test that the `can_download_remote` property throws an exception when the value is not a boolean """ # GIVEN: A RemotePage object with a mocked out download_checkbox remote_page = RemotePage(None) remote_page.download_checkbox = MagicMock() # WHEN: The can_download_remote property is set # THEN: An exception is thrown with pytest.raises(TypeError, match='Must be a bool'): remote_page.can_download_remote = 'not a bool' def test_theme_list_widget_resize(ftf_app): """ Test that the resizeEvent() method in the ThemeListWidget works correctly """ # GIVEN: An instance of the ThemeListWidget widget = ThemeListWidget(None) # WHEN: resizeEvent() is called with patch.object(widget, 'viewport') as mocked_viewport, \ patch.object(widget, 'setGridSize') as mocked_setGridSize: mocked_viewport.return_value.width.return_value = 600 widget.resizeEvent(None) # THEN: Check that the correct calculations were done mocked_setGridSize.assert_called_once_with(QtCore.QSize(149, 140)) def test_failed_download(mocked_set_icon): """ Test that icon get set to indicate a failure when `DownloadWorker` emits the download_failed signal """ # GIVEN: An instance of `DownloadWorker` instance = ThemeListWidgetItem('url', sample_theme_data, MagicMock()) # noqa Overcome GC issue worker_threads = Registry().get('application').worker_threads worker = worker_threads['thumbnail_download_BlueBurst.png']['worker'] # WHEN: `DownloadWorker` emits the `download_failed` signal worker.download_failed.emit() # THEN: Then the initial loading icon should have been replaced by the exception icon mocked_set_icon.assert_has_calls([call(UiIcons().picture), call(UiIcons().exception)]) @patch('openlp.core.ui.firsttimeform.build_icon') def test_successful_download(mocked_build_icon, mocked_set_icon): """ Test that the downloaded thumbnail is set as the icon when `DownloadWorker` emits the `download_succeeded` signal """ # GIVEN: An instance of `DownloadWorker` instance = ThemeListWidgetItem('url', sample_theme_data, MagicMock()) # noqa Overcome GC issue worker_threads = Registry().get('application').worker_threads worker = worker_threads['thumbnail_download_BlueBurst.png']['worker'] test_path = Path('downlaoded', 'file') # WHEN: `DownloadWorker` emits the `download_succeeded` signal worker.download_succeeded.emit(test_path) # THEN: An icon should have been built from the downloaded file and used to replace the loading icon mocked_build_icon.assert_called_once_with(test_path) mocked_set_icon.assert_has_calls([call(UiIcons().picture), call(mocked_build_icon())]) @patch.object(FirstTimeForm, 'provide_help') def test_help(mocked_help, settings): """ Test the help button """ # GIVEN: A First Time Wizard and a patched help function frw = FirstTimeForm(None) # WHEN: The Help button is clicked QtTest.QTest.mouseClick(frw.button(QtWidgets.QWizard.HelpButton), QtCore.Qt.LeftButton) # THEN: The Help function should be called mocked_help.assert_called_once()