diff --git a/openlp/core/projectors/manager.py b/openlp/core/projectors/manager.py
index 2db1d2966..da1ff4dee 100644
--- a/openlp/core/projectors/manager.py
+++ b/openlp/core/projectors/manager.py
@@ -437,57 +437,44 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
"""
self.projector_form.exec()
- def on_blank_projector(self, opt=None):
+ def on_blank_projector(self, item=None, opt=None):
"""
- Calls projector thread to send blank screen command
+ Calls projector(s) thread to send blank screen command
- :param opt: Needed by PyQt5
+ :param item: Optional ProjectorItem() instance in case of direct call
+ :param opt: (Deprecated)
"""
- try:
- opt.link.set_shutter_closed()
- except AttributeError:
- for list_item in self.projector_list_widget.selectedItems():
- if list_item is None:
- return
- projector = list_item.data(QtCore.Qt.UserRole)
- try:
- projector.link.set_shutter_closed()
- except Exception:
- continue
+ if item is not None:
+ return item.pjlink.set_shutter_closed()
+ for list_item in self.projector_list_widget.selectedItems():
+ list_item.data(QtCore.Qt.UserRole).pjlink.set_shutter_closed()
- def on_doubleclick_item(self, item, opt=None):
+ def on_doubleclick_item(self, item):
"""
When item is doubleclicked, will connect to projector.
:param item: List widget item for connection.
- :param opt: Needed by PyQt5
"""
projector = item.data(QtCore.Qt.UserRole)
- if QSOCKET_STATE[projector.link.state()] != S_CONNECTED:
- try:
- log.debug(f'ProjectorManager: Calling connect_to_host() on "{projector.link.ip}"')
- projector.link.connect_to_host()
- except Exception:
- log.debug(f'ProjectorManager: "{projector.link.ip}" already connected - skipping')
+ if QSOCKET_STATE[projector.link.state()] == S_CONNECTED:
+ log.debug(f'ProjectorManager: "{projector.pjlink.name}" already connected - skipping')
+ else:
+ log.debug(f'ProjectorManager: "{projector.pjlink.name}" calling connect_to_host()')
+ projector.link.connect_to_host()
return
- def on_connect_projector(self, opt=None):
+ def on_connect_projector(self, item=None, opt=None):
"""
Calls projector thread to connect to projector
- :param opt: Needed by PyQt5
+ :param item: (Optional) ProjectorItem() for direct call
+ :param opt: (Deprecated)
"""
- try:
- opt.link.connect_to_host()
- except AttributeError:
+ if item is not None:
+ return item.pjlink.connect_to_host()
+ else:
for list_item in self.projector_list_widget.selectedItems():
- if list_item is None:
- return
- projector = list_item.data(QtCore.Qt.UserRole)
- try:
- projector.link.connect_to_host()
- except Exception:
- continue
+ list_item.data(QtCore.Qt.UserRole).pjlink.connect_to_host()
def on_delete_projector(self, opt=None):
"""
@@ -553,23 +540,18 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
log.debug(f'New projector list - item: {item.link.ip} {item.link.name}')
self.udp_listen_delete(old_port)
- def on_disconnect_projector(self, opt=None):
+ def on_disconnect_projector(self, item=None, opt=None):
"""
Calls projector thread to disconnect from projector
- :param opt: Needed by PyQt5
+ :param item: (Optional) ProjectorItem() for direct call
+ :param opt: (Deprecated)
"""
- try:
- opt.link.disconnect_from_host()
- except AttributeError:
+ if item is not None:
+ return item.pjlink.disconnect_from_host()
+ else:
for list_item in self.projector_list_widget.selectedItems():
- if list_item is None:
- return
- projector = list_item.data(QtCore.Qt.UserRole)
- try:
- projector.link.disconnect_from_host()
- except Exception:
- continue
+ list_item.data(QtCore.Qt.UserRole).pjlink.disconnect_from_host()
def on_edit_projector(self, opt=None):
"""
@@ -586,59 +568,44 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
self.projector_form.exec(projector.db_item)
projector.db_item = self.projectordb.get_projector_by_id(self.old_projector.db_item.id)
- def on_poweroff_projector(self, opt=None):
+ def on_poweroff_projector(self, item=None, opt=None):
"""
- Calls projector link to send Power Off command
+ Calls projector thread to turn projector off
- :param opt: Needed by PyQt5
+ :param item: (Optional) ProjectorItem() for direct call
+ :param opt: (Deprecated)
"""
- try:
- opt.link.set_power_off()
- except AttributeError:
+ if item is not None:
+ return item.pjlink.set_power_off()
+ else:
for list_item in self.projector_list_widget.selectedItems():
- if list_item is None:
- return
- projector = list_item.data(QtCore.Qt.UserRole)
- try:
- projector.link.set_power_off()
- except Exception:
- continue
+ list_item.data(QtCore.Qt.UserRole).pjlink.set_power_off()
- def on_poweron_projector(self, opt=None):
+ def on_poweron_projector(self, item=None, opt=None):
"""
- Calls projector link to send Power On command
+ Calls projector thread to turn projector on
- :param opt: Needed by PyQt5
+ :param item: (Optional) ProjectorItem() for direct call
+ :param opt: (Deprecated)
"""
- try:
- opt.link.set_power_on()
- except AttributeError:
+ if item is not None:
+ return item.pjlink.set_power_on()
+ else:
for list_item in self.projector_list_widget.selectedItems():
- if list_item is None:
- return
- projector = list_item.data(QtCore.Qt.UserRole)
- try:
- projector.link.set_power_on()
- except Exception:
- continue
+ list_item.data(QtCore.Qt.UserRole).pjlink.set_power_on()
- def on_show_projector(self, opt=None):
+ def on_show_projector(self, item=None, opt=None):
"""
- Calls projector thread to send open shutter command
+ Calls projector thread to open shutter
- :param opt: Needed by PyQt5
+ :param item: (Optional) ProjectorItem() for direct call
+ :param opt: (Deprecated)
"""
- try:
- opt.link.set_shutter_open()
- except AttributeError:
+ if item is not None:
+ return item.pjlink.set_shutter_open()
+ else:
for list_item in self.projector_list_widget.selectedItems():
- if list_item is None:
- return
- projector = list_item.data(QtCore.Qt.UserRole)
- try:
- projector.link.set_shutter_open()
- except Exception:
- continue
+ list_item.data(QtCore.Qt.UserRole).pjlink.set_shutter_open()
def on_status_projector(self, opt=None):
"""
@@ -740,27 +707,27 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
:param projector: Projector instance to add
:param start: Start projector if True
"""
- item = ProjectorItem(link=self._add_projector(projector))
+ item = ProjectorItem(parent=self, item=self._add_projector(projector))
item.db_item = projector
item.icon = QtGui.QIcon(self.status_icons[S_NOT_CONNECTED])
widget = QtWidgets.QListWidgetItem(item.icon,
- item.link.name,
+ item.pjlink.name,
self.projector_list_widget
)
widget.setData(QtCore.Qt.UserRole, item)
- item.link.db_item = item.db_item
+ item.pjlink.db_item = item.db_item
item.widget = widget
- item.link.changeStatus.connect(self.update_status)
- item.link.projectorAuthentication.connect(self.authentication_error)
- item.link.projectorNoAuthentication.connect(self.no_authentication_error)
- item.link.projectorUpdateIcons.connect(self.update_icons)
+ item.pjlink.changeStatus.connect(self.update_status)
+ item.pjlink.projectorAuthentication.connect(self.authentication_error)
+ item.pjlink.projectorNoAuthentication.connect(self.no_authentication_error)
+ item.pjlink.projectorUpdateIcons.connect(self.update_icons)
# Add UDP listener for new projector port
- self.udp_listen_add(item.link.port)
+ self.udp_listen_add(item.pjlink.port)
self.projector_list.append(item)
if start:
- item.link.connect_to_host()
+ item.pjlink.connect_to_host()
for item in self.projector_list:
- log.debug(f'New projector list - item: ({item.link.ip}) {item.link.name}')
+ log.debug(f'New projector list - item: ({item.pjlink.ip}) {item.pjlink.name}')
@QtCore.pyqtSlot(str)
def add_projector_from_wizard(self, ip, opts=None):
@@ -969,13 +936,20 @@ class ProjectorItem(QtCore.QObject):
Class for the projector list widget item.
NOTE: Actual PJLink class instance should be saved as self.link
"""
- def __init__(self, link=None):
+ def __init__(self, parent=None, link=None, item=None):
"""
Initialization for ProjectorItem instance
- :param link: PJLink instance for QListWidgetItem
+ :param link: (Deprecated) PJLink instance for QListWidgetItem
+ :param item: PJLink instance for QListWidgetItem
"""
- self.link = link
+ # Refactor so self.link is renamed self.pjlink to clarify reference
+ if link is None:
+ self.link = item
+ self.pjlink = item
+ else:
+ self.link = link
+ self.pjlink = link
self.thread = None
self.icon = None
self.widget = None
diff --git a/tests/helpers/projector.py b/tests/helpers/projector.py
new file mode 100644
index 000000000..3191863c5
--- /dev/null
+++ b/tests/helpers/projector.py
@@ -0,0 +1,82 @@
+# -*- 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 . #
+##########################################################################
+"""
+Help classes/functions for PJLink Projector tests
+"""
+
+from unittest.mock import MagicMock
+from PyQt5 import QtNetwork
+
+from openlp.core.projectors.constants import S_OK, S_NOT_CONNECTED
+from openlp.core.projectors.db import Projector
+from tests.resources.projector.data import TEST1_DATA
+
+
+class FakePJLink(object):
+ def __init__(self, projector=None, *args, **kwargs):
+ # Signal mocks
+ self.projectorStatus = MagicMock()
+ self.projectorAuthentication = MagicMock()
+ self.projectorNoAuthentication = MagicMock()
+ self.projectorReceivedData = MagicMock()
+ self.projectorUpdateIcons = MagicMock()
+
+ # Method mocks
+ self.changeStatus = MagicMock()
+ self.connect_to_host = MagicMock()
+ self.disconnect_from_host = MagicMock()
+ self.poll_timer = MagicMock()
+ self.set_power_off = MagicMock()
+ self.set_power_on = MagicMock()
+ self.set_shutter_closed = MagicMock()
+ self.set_shutter_open = MagicMock()
+ self.socket_timer = MagicMock()
+ self.status_timer = MagicMock()
+ self.state = MagicMock()
+
+ # Some tests that may include what it thinks are ProjectorItem()
+ # If ProjectorItem() is called, will probably overwrite these - OK
+ self.link = self
+ self.pjlink = self
+
+ # Normal entries from PJLink
+ self.entry = Projector(**TEST1_DATA) if projector is None else projector
+ self.ip = self.entry.ip
+ self.qhost = QtNetwork.QHostAddress(self.ip)
+ self.location = self.entry.location
+ self.mac_adx = self.entry.mac_adx
+ self.name = self.entry.name
+ self.notes = self.entry.notes
+ self.pin = self.entry.pin
+ self.port = int(self.entry.port)
+ self.pjlink_class = "1" if self.entry.pjlink_class is None else self.entry.pjlink_class
+ self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000
+ self.socket_timeout = 5000 if 'socket_timeout' not in kwargs else kwargs['socket_timeout'] * 1000
+ self.no_poll = 'no_poll' in kwargs
+ self.status_connect = S_NOT_CONNECTED
+ self.last_command = ''
+ self.projector_status = S_NOT_CONNECTED
+ self.error_status = S_OK
+ self.send_queue = []
+ self.priority_queue = []
+ self.send_busy = False
+ self.status_timer_checks = {} # Keep track of events for the status timer
+ # Default mock return values
diff --git a/tests/openlp_core/projectors/manager/test_misc_manager.py b/tests/openlp_core/projectors/manager/test_misc_manager.py
index cec3200d8..8fbfe7f0b 100644
--- a/tests/openlp_core/projectors/manager/test_misc_manager.py
+++ b/tests/openlp_core/projectors/manager/test_misc_manager.py
@@ -20,6 +20,11 @@
##########################################################################
"""
Test misc. functions with few test paths
+
+_load_projectors()
+add_projector_from_wizard()
+get_projector_list()
+
"""
from unittest.mock import DEFAULT, patch
@@ -47,9 +52,7 @@ def test_private_load_projectors(projector_manager_mtdb):
with patch.multiple(projector_manager_mtdb,
udp_listen_add=DEFAULT,
udp_listen_delete=DEFAULT,
- _load_projectors=DEFAULT) as mock_manager:
- # Satisfy Flake8 linting
- mock_manager['udp_listen_add'].return_value = None
+ _load_projectors=DEFAULT):
projector_manager_mtdb.bootstrap_initialise()
projector_manager_mtdb.bootstrap_post_set_up()
@@ -67,44 +70,6 @@ def test_private_load_projectors(projector_manager_mtdb):
assert t_chk == t_list, 'projector_list DB items do not match test items'
-def test_on_edit_input(projector_manager):
- """
- Test calling edit projector input GUI from input selection icon makes appropriate calls
- """
- # GIVEN: Test environment
- with patch.object(projector_manager, 'on_select_input') as mock_edit:
-
- # WHEN: Called
- projector_manager.on_edit_input()
-
- # THEN: select input called with edit option
- mock_edit.assert_called_with(opt=None, edit=True)
-
-
-def test_on_add_projector(projector_manager):
- """
- Test add new projector edit GUI is called properly
- """
- # GIVEN: Test environment
- # Mock to keep from getting event not registered error in Registry()
- # during bootstrap_post_set_up()
- with patch.multiple(projector_manager,
- udp_listen_add=DEFAULT,
- udp_listen_delete=DEFAULT) as mock_manager:
- # Satisfy Flake8 linting
- mock_manager['udp_listen_add'].return_value = None
- projector_manager.bootstrap_initialise()
- projector_manager.bootstrap_post_set_up()
-
- with patch.object(projector_manager, 'projector_form') as mock_form:
-
- # WHEN called
- projector_manager.on_add_projector()
-
- # THEN: projector form called
- mock_form.exec.assert_called_once()
-
-
def test_add_projector_from_wizard(projector_manager):
"""
Test when add projector from GUI, appropriate method is called correctly
@@ -138,9 +103,7 @@ def test_get_projector_list(projector_manager_mtdb):
# during bootstrap_post_set_up()
with patch.multiple(projector_manager_mtdb,
udp_listen_add=DEFAULT,
- udp_listen_delete=DEFAULT) as mock_manager:
- # Satisfy Flake8 linting
- mock_manager['udp_listen_add'].return_value = None
+ udp_listen_delete=DEFAULT):
projector_manager_mtdb.bootstrap_initialise()
projector_manager_mtdb.bootstrap_post_set_up()
diff --git a/tests/openlp_core/projectors/manager/test_toolbar_triggers-01.py b/tests/openlp_core/projectors/manager/test_toolbar_triggers-01.py
new file mode 100644
index 000000000..a7db4ebff
--- /dev/null
+++ b/tests/openlp_core/projectors/manager/test_toolbar_triggers-01.py
@@ -0,0 +1,511 @@
+# -*- 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 . #
+##########################################################################
+"""
+Test methods called by toolbar icons part 1
+
+on_blank_projector()
+on_connect_projector()
+on_disconnect_projector()
+on_poweroff_projector()
+on_poweron_projector()
+on_show_projector()
+
+"""
+
+from unittest.mock import DEFAULT, patch
+
+from openlp.core.projectors.db import Projector
+
+from tests.helpers.projector import FakePJLink
+from tests.resources.projector.data import TEST1_DATA, TEST2_DATA, TEST3_DATA
+
+
+def helper_method(test_fixture, method, effect, test_item=None):
+ """
+ Boilerplate for tests
+
+ :param test_fixture: Fixture used for testing
+ :param method: Method in fixture to test
+ :param effect: Dict of items for testing
+ {'item': Item class
+ 'select': Boolean to enable selected() in projector_list_widget
+ :param test_item: (Optional) Item to call method with
+
+ """
+ with patch.multiple(test_fixture,
+ udp_listen_add=DEFAULT,
+ udp_listen_delete=DEFAULT,
+ update_icons=DEFAULT,
+ _add_projector=DEFAULT) as mock_manager:
+
+ mock_manager['_add_projector'].side_effect = [item['item'] for item in effect]
+ test_fixture.bootstrap_initialise()
+ # projector_list_widget created here
+ test_fixture.bootstrap_post_set_up()
+
+ # Add ProjectorItem instances to projector_list_widget
+ for item in effect:
+ test_fixture.add_projector(projector=item['item'])
+
+ # Set at least one instance as selected to verify projector_list_widget is not called
+ for item in range(len(effect)):
+ test_fixture.projector_list_widget.item(item).setSelected(effect[item]['select'])
+
+ # WHEN: Called with projector instance
+ method(item=test_item)
+
+
+def test_on_blank_projector_direct(projector_manager_mtdb):
+ """
+ Test calling method directly - projector_list_widget should not be called
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_blank_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ],
+ test_item=t_1
+ )
+
+ # THEN: Only t_1.set_shutter_closed() should be called
+ t_1.set_shutter_closed.assert_called_once()
+ t_2.set_shutter_closed.assert_not_called()
+ t_3.set_shutter_closed.assert_not_called()
+
+
+def test_on_blank_projector_one_item(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_blank_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': True},
+ {'item': t_3, 'select': False}
+ ]
+ )
+
+ # THEN: Only t_3.set_shutter_closed() should be called
+ t_1.set_shutter_closed.assert_not_called()
+ t_2.set_shutter_closed.assert_called_once()
+ t_3.set_shutter_closed.assert_not_called()
+
+
+def test_on_blank_projector_multiple_items(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with more than one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_blank_projector,
+ effect=[{'item': t_1, 'select': True},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: t_1 and t_3 set_shutter_closed() should be called
+ t_1.set_shutter_closed.assert_called_once()
+ t_2.set_shutter_closed.assert_not_called()
+ t_3.set_shutter_closed.assert_called_once()
+
+
+def test_on_connect_projector_direct(projector_manager_mtdb):
+ """
+ Test calling method directly - projector_list_widget should not be called
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_connect_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ],
+ test_item=t_1
+ )
+
+ # THEN: Only t_1.connect_to_host() should be called
+ t_1.connect_to_host.assert_called_once()
+ t_2.connect_to_host.assert_not_called()
+ t_3.connect_to_host.assert_not_called()
+
+
+def test_on_connect_projector_one_item(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_connect_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: Only t_3.connect_to_host() should be called
+ t_1.connect_to_host.assert_not_called()
+ t_2.connect_to_host.assert_not_called()
+ t_3.connect_to_host.assert_called_once()
+
+
+def test_on_connect_projector_multiple_items(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with more than one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_connect_projector,
+ effect=[{'item': t_1, 'select': True},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: t_1 and t_3 connect_to_host() should be called
+ t_1.connect_to_host.assert_called_once()
+ t_2.connect_to_host.assert_not_called()
+ t_3.connect_to_host.assert_called_once()
+
+
+def test_on_disconnect_projector_direct(projector_manager_mtdb):
+ """
+ Test calling method directly - projector_list_widget should not be called
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_disconnect_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ],
+ test_item=t_1
+ )
+
+ # THEN: Only t_1.disconnect_from_host() should be called
+ t_1.disconnect_from_host.assert_called_once()
+ t_2.disconnect_from_host.assert_not_called()
+ t_3.disconnect_from_host.assert_not_called()
+
+
+def test_on_disconnect_projector_one_item(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_disconnect_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: Only t_3.disconnect_from_host() should be called
+ t_1.disconnect_from_host.assert_not_called()
+ t_2.disconnect_from_host.assert_not_called()
+ t_3.disconnect_from_host.assert_called_once()
+
+
+def test_on_disconnect_projector_multiple_items(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with more than one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_disconnect_projector,
+ effect=[{'item': t_1, 'select': True},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: t_1 and t_3 disconnect_from_host() should be called
+ t_1.disconnect_from_host.assert_called_once()
+ t_2.disconnect_from_host.assert_not_called()
+ t_3.disconnect_from_host.assert_called_once()
+
+
+def test_on_poweroff_projector_direct(projector_manager_mtdb):
+ """
+ Test calling method directly - projector_list_widget should not be called
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_poweroff_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ],
+ test_item=t_1
+ )
+
+ # THEN: Only t_1.set_power_off() should be called
+ t_1.set_power_off.assert_called_once()
+ t_2.set_power_off.assert_not_called()
+ t_3.set_power_off.assert_not_called()
+
+
+def test_on_poweroff_projector_one_item(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_poweroff_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: Only t_3.set_power_off() should be called
+ t_1.set_power_off.assert_not_called()
+ t_2.set_power_off.assert_not_called()
+ t_3.set_power_off.assert_called_once()
+
+
+def test_on_poweroff_projector_multiple_items(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with more than one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_poweroff_projector,
+ effect=[{'item': t_1, 'select': True},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: t_1 and t_3 set_power_off() should be called
+ t_1.set_power_off.assert_called_once()
+ t_2.set_power_off.assert_not_called()
+ t_3.set_power_off.assert_called_once()
+
+
+def test_on_poweron_projector_direct(projector_manager_mtdb):
+ """
+ Test calling method directly - projector_list_widget should not be called
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_poweron_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ],
+ test_item=t_1
+ )
+
+ # THEN: Only t_1.set_power_on() should be called
+ t_1.set_power_on.assert_called_once()
+ t_2.set_power_on.assert_not_called()
+ t_3.set_power_on.assert_not_called()
+
+
+def test_on_poweron_projector_one_item(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_poweron_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: Only t_3.set_power_on() should be called
+ t_1.set_power_on.assert_not_called()
+ t_2.set_power_on.assert_not_called()
+ t_3.set_power_on.assert_called_once()
+
+
+def test_on_poweron_projector_multiple_items(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with more than one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_poweron_projector,
+ effect=[{'item': t_1, 'select': True},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: t_1 and t_3 set_power_on() should be called
+ t_1.set_power_on.assert_called_once()
+ t_2.set_power_on.assert_not_called()
+ t_3.set_power_on.assert_called_once()
+
+
+def test_on_show_projector_direct(projector_manager_mtdb):
+ """
+ Test calling method directly - projector_list_widget should not be called
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_show_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ],
+ test_item=t_1
+ )
+
+ # THEN: Only t_1.set_shutter_open() should be called
+ t_1.set_shutter_open.assert_called_once()
+ t_2.set_shutter_open.assert_not_called()
+ t_3.set_shutter_open.assert_not_called()
+
+
+def test_on_show_projector_one_item(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_show_projector,
+ effect=[{'item': t_1, 'select': False},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: Only t_3.set_shutter_open() should be called
+ t_1.set_shutter_open.assert_not_called()
+ t_2.set_shutter_open.assert_not_called()
+ t_3.set_shutter_open.assert_called_once()
+
+
+def test_on_show_projector_multiple_items(projector_manager_mtdb):
+ """
+ Test calling method using projector_list_widget with more than one item selected
+ """
+ # GIVEN: Test setup
+ t_1 = FakePJLink(Projector(**TEST1_DATA))
+ t_2 = FakePJLink(Projector(**TEST2_DATA))
+ t_3 = FakePJLink(Projector(**TEST3_DATA))
+
+ # WHEN: called
+ helper_method(test_fixture=projector_manager_mtdb,
+ method=projector_manager_mtdb.on_show_projector,
+ effect=[{'item': t_1, 'select': True},
+ {'item': t_2, 'select': False},
+ {'item': t_3, 'select': True}
+ ]
+ )
+
+ # THEN: t_1 and t_3 set_shutter_open() should be called
+ t_1.set_shutter_open.assert_called_once()
+ t_2.set_shutter_open.assert_not_called()
+ t_3.set_shutter_open.assert_called_once()
diff --git a/tests/openlp_core/projectors/manager/test_toolbar_triggers-basic.py b/tests/openlp_core/projectors/manager/test_toolbar_triggers-basic.py
new file mode 100644
index 000000000..5504455cc
--- /dev/null
+++ b/tests/openlp_core/projectors/manager/test_toolbar_triggers-basic.py
@@ -0,0 +1,141 @@
+# -*- 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 . #
+##########################################################################
+"""
+Test methods called by toolbar icons with minimal flow paths and tests
+
+on_add_projector()
+on_doubleclick_item()
+on_edit_input()
+
+"""
+
+import logging
+import openlp.core.projectors.manager
+
+from unittest.mock import DEFAULT, patch
+
+from openlp.core.projectors.constants import QSOCKET_STATE, \
+ S_CONNECTED, S_NOT_CONNECTED
+
+from tests.helpers.projector import FakePJLink
+
+test_module = openlp.core.projectors.manager.__name__
+
+
+def test_on_add_projector(projector_manager):
+ """
+ Test add new projector edit GUI is called properly
+ """
+ # GIVEN: Test environment
+ # Mock to keep from getting event not registered error in Registry()
+ # during bootstrap_post_set_up()
+ with patch.multiple(projector_manager,
+ udp_listen_add=DEFAULT,
+ udp_listen_delete=DEFAULT):
+ projector_manager.bootstrap_initialise()
+ projector_manager.bootstrap_post_set_up()
+
+ # Have to wait for projector_manager.bootstrap_post_set_up() before projector_form is initialized
+ with patch.object(projector_manager, 'projector_form') as mock_form:
+
+ # WHEN called
+ projector_manager.on_add_projector()
+
+ # THEN: projector form called
+ mock_form.exec.assert_called_once()
+
+
+def test_on_edit_input(projector_manager, pjlink):
+ """
+ Test on_edit_input calls on_select_input properly
+ """
+ # GIVEN: Test setup
+ with patch.object(projector_manager, 'on_select_input') as mock_input:
+
+ # WHEN: Called with pjlink instance
+ projector_manager.on_edit_input(opt=pjlink)
+
+ # THEN: appropriate call made
+ mock_input.assert_called_with(opt=pjlink, edit=True)
+
+
+def test_on_doubleclick_item_connected(projector_manager_mtdb, caplog):
+ """
+ Test projector.connect_to_host() not called when status is S_CONNECTED
+ """
+ t_1 = FakePJLink()
+ caplog.set_level(logging.DEBUG)
+ logs = [(test_module, logging.DEBUG,
+ f'ProjectorManager: "{t_1.pjlink.name}" already connected - skipping')]
+
+ with patch.multiple(projector_manager_mtdb,
+ udp_listen_add=DEFAULT,
+ udp_listen_delete=DEFAULT,
+ update_icons=DEFAULT,
+ _add_projector=DEFAULT) as mock_manager:
+
+ projector_manager_mtdb.bootstrap_initialise()
+ # projector_list_widget created here
+ projector_manager_mtdb.bootstrap_post_set_up()
+
+ # Add ProjectorItem instances to projector_list_widget
+ mock_manager['_add_projector'].return_value = t_1
+ projector_manager_mtdb.add_projector(projector=t_1)
+
+ # WHEN: Called
+ t_1.state.return_value = QSOCKET_STATE[S_CONNECTED]
+ caplog.clear()
+ projector_manager_mtdb.on_doubleclick_item(projector_manager_mtdb.projector_list_widget.item(0))
+
+ assert caplog.record_tuples == logs, 'Invalid log entries'
+ t_1.connect_to_host.assert_not_called()
+
+
+def test_on_doubleclick_item_not_connected(projector_manager_mtdb, caplog):
+ """
+ Test projector.connect_to_host() called
+ """
+ t_1 = FakePJLink()
+ caplog.set_level(logging.DEBUG)
+ logs = [(test_module, logging.DEBUG,
+ f'ProjectorManager: "{t_1.pjlink.name}" calling connect_to_host()')]
+
+ with patch.multiple(projector_manager_mtdb,
+ udp_listen_add=DEFAULT,
+ udp_listen_delete=DEFAULT,
+ update_icons=DEFAULT,
+ _add_projector=DEFAULT) as mock_manager:
+
+ projector_manager_mtdb.bootstrap_initialise()
+ # projector_list_widget created here
+ projector_manager_mtdb.bootstrap_post_set_up()
+
+ # Add ProjectorItem instances to projector_list_widget
+ mock_manager['_add_projector'].return_value = t_1
+ projector_manager_mtdb.add_projector(projector=t_1)
+
+ # WHEN: Called
+ t_1.state.return_value = QSOCKET_STATE[S_NOT_CONNECTED]
+ caplog.clear()
+ projector_manager_mtdb.on_doubleclick_item(projector_manager_mtdb.projector_list_widget.item(0))
+
+ assert caplog.record_tuples == logs, 'Invalid log entries'
+ t_1.connect_to_host.assert_called_once()
diff --git a/tests/resources/projector/data.py b/tests/resources/projector/data.py
index 33e208eaa..7910bcb05 100644
--- a/tests/resources/projector/data.py
+++ b/tests/resources/projector/data.py
@@ -35,7 +35,8 @@ TEST_HASH = '5d8409bc1c3fa39749434aa3a5c38682'
TEST_CONNECT_AUTHENTICATE = 'PJLink 1 {salt}'.format(salt=TEST_SALT)
-TEST1_DATA = dict(ip='111.111.111.111',
+TEST1_DATA = dict(id=1,
+ ip='111.111.111.111',
port='1111',
pin='1111',
name='___TEST_ONE___',
@@ -47,7 +48,8 @@ TEST1_DATA = dict(ip='111.111.111.111',
model_lamp='Lamp type 1',
mac_adx='11:11:11:11:11:11')
-TEST2_DATA = dict(ip='222.222.222.222',
+TEST2_DATA = dict(id=2,
+ ip='222.222.222.222',
port='2222',
pin='2222',
name='___TEST_TWO___',
@@ -59,7 +61,8 @@ TEST2_DATA = dict(ip='222.222.222.222',
model_lamp='Lamp type 2',
mac_adx='22:22:22:22:22:22')
-TEST3_DATA = dict(ip='333.333.333.333',
+TEST3_DATA = dict(id=3,
+ ip='333.333.333.333',
port='3333',
pin='3333',
name='___TEST_THREE___',