410 lines
13 KiB
Python
410 lines
13 KiB
Python
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
from cthulhu import notification_presenter
|
|
from cthulhu import messages
|
|
from cthulhu.notification_presenter import NotificationPresenter
|
|
|
|
|
|
class _FakeMonitor:
|
|
def __init__(self):
|
|
self.dismissed = []
|
|
self.invoked = []
|
|
self.current_generation = 2
|
|
|
|
def refresh(self, announce_new=False):
|
|
return True
|
|
|
|
def is_current_entry(self, entry):
|
|
return entry.source == "mako" and entry.source_generation == self.current_generation
|
|
|
|
def dismiss_notification(self, notification_id):
|
|
self.dismissed.append(notification_id)
|
|
return True
|
|
|
|
def invoke_action(self, notification_id, action_key):
|
|
self.invoked.append((notification_id, action_key))
|
|
return True
|
|
|
|
|
|
class _FakeButton:
|
|
def __init__(self, label=None):
|
|
self.label = label
|
|
self.sensitive = True
|
|
self._signals = {}
|
|
|
|
def connect(self, signal_name, callback, *args):
|
|
self._signals[signal_name] = (callback, args)
|
|
|
|
def set_sensitive(self, value):
|
|
self.sensitive = value
|
|
|
|
def set_margin_top(self, _value):
|
|
return None
|
|
|
|
def click(self):
|
|
callback, args = self._signals["clicked"]
|
|
callback(self, *args)
|
|
|
|
|
|
class _FakeBox:
|
|
def __init__(self):
|
|
self.children = []
|
|
|
|
def get_children(self):
|
|
return list(self.children)
|
|
|
|
def remove(self, child):
|
|
self.children.remove(child)
|
|
|
|
def pack_start(self, child, expand, fill, padding):
|
|
del expand, fill, padding
|
|
self.children.append(child)
|
|
|
|
def show_all(self):
|
|
return None
|
|
|
|
|
|
class _FakeLabel:
|
|
def __init__(self):
|
|
self.text = ""
|
|
self.visible = False
|
|
|
|
def set_text(self, value):
|
|
self.text = value
|
|
|
|
def set_visible(self, value):
|
|
self.visible = value
|
|
|
|
|
|
class _FakeListStore:
|
|
def __init__(self, rows=None):
|
|
self.rows = list(rows or [])
|
|
|
|
def iter_n_children(self, _parent):
|
|
return len(self.rows)
|
|
|
|
def get_iter(self, path):
|
|
index = path if isinstance(path, int) else path[0]
|
|
if 0 <= index < len(self.rows):
|
|
return index
|
|
return None
|
|
|
|
def get_value(self, row_iter, column):
|
|
return self.rows[row_iter][column]
|
|
|
|
def remove(self, row_iter):
|
|
del self.rows[row_iter]
|
|
return row_iter < len(self.rows)
|
|
|
|
|
|
class _FakeSelection:
|
|
def __init__(self, model=None, selected_path=0):
|
|
self.model = model
|
|
self.selected_path = selected_path
|
|
|
|
def get_selected_rows(self):
|
|
if self.selected_path is None:
|
|
return self.model, []
|
|
return self.model, [self.selected_path]
|
|
|
|
def select_path(self, path):
|
|
self.selected_path = path
|
|
|
|
|
|
class NotificationPresenterMakoTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.presenter = NotificationPresenter()
|
|
self.monitor = _FakeMonitor()
|
|
self.presenter.set_mako_monitor(self.monitor)
|
|
|
|
def test_sync_updates_current_generation_only(self):
|
|
old_entry = self.presenter.save_notification(
|
|
"Notification old",
|
|
source="mako",
|
|
source_generation=1,
|
|
notification_id=5,
|
|
live=False,
|
|
actions={"default": "Old"},
|
|
)
|
|
current_entry = self.presenter.save_notification(
|
|
"Notification current",
|
|
source="mako",
|
|
source_generation=2,
|
|
notification_id=6,
|
|
live=True,
|
|
actions={"default": "View"},
|
|
)
|
|
|
|
self.presenter.sync_live_notifications(
|
|
"mako",
|
|
{
|
|
5: {
|
|
"message": "Notification replaced",
|
|
"actions": {"default": "New"},
|
|
"app_name": "discord",
|
|
"summary": "replaced",
|
|
"body": "",
|
|
"urgency": 1,
|
|
"desktop_entry": "discord",
|
|
}
|
|
},
|
|
source_generation=2,
|
|
)
|
|
|
|
self.assertEqual(old_entry.message, "Notification old")
|
|
self.assertEqual(old_entry.actions, {"default": "Old"})
|
|
self.assertFalse(current_entry.live)
|
|
|
|
def test_can_control_entry_and_get_actions_require_current_monitor_generation(self):
|
|
entry = self.presenter.save_notification(
|
|
"Notification current",
|
|
source="mako",
|
|
source_generation=2,
|
|
notification_id=6,
|
|
live=True,
|
|
actions={"default": "View"},
|
|
)
|
|
|
|
self.assertTrue(self.presenter.can_control_entry(entry))
|
|
self.assertEqual(self.presenter.get_actions_for_entry(entry), {"default": "View"})
|
|
|
|
stale_entry = self.presenter.save_notification(
|
|
"Notification stale",
|
|
source="mako",
|
|
source_generation=1,
|
|
notification_id=7,
|
|
live=True,
|
|
actions={"default": "Open"},
|
|
)
|
|
|
|
self.assertFalse(self.presenter.can_control_entry(stale_entry))
|
|
self.assertEqual(self.presenter.get_actions_for_entry(stale_entry), {})
|
|
|
|
def test_dismiss_and_invoke_action_route_through_monitor(self):
|
|
entry = self.presenter.save_notification(
|
|
"Notification current",
|
|
source="mako",
|
|
source_generation=2,
|
|
notification_id=6,
|
|
live=True,
|
|
actions={"default": "View"},
|
|
)
|
|
script = mock.Mock()
|
|
|
|
self.assertTrue(self.presenter.dismiss_entry(script, entry))
|
|
self.assertEqual(self.monitor.dismissed, [6])
|
|
self.assertEqual(self.presenter._notifications, [])
|
|
script.presentMessage.assert_called_with(messages.NOTIFICATION_DISMISSED)
|
|
|
|
script.reset_mock()
|
|
entry = self.presenter.save_notification(
|
|
"Notification current",
|
|
source="mako",
|
|
source_generation=2,
|
|
notification_id=6,
|
|
live=True,
|
|
actions={"default": "View"},
|
|
)
|
|
self.assertTrue(self.presenter.invoke_action_for_entry(script, entry, "default"))
|
|
self.assertEqual(self.monitor.invoked, [(6, "default")])
|
|
script.presentMessage.assert_called_with(messages.NOTIFICATION_ACTION_INVOKED)
|
|
|
|
def test_notification_list_dismiss_removes_selected_entry_from_history_and_model(self):
|
|
entry = self.presenter.save_notification(
|
|
"Notification current",
|
|
source="mako",
|
|
source_generation=2,
|
|
notification_id=6,
|
|
live=True,
|
|
actions={"default": "View"},
|
|
)
|
|
remaining = self.presenter.save_notification(
|
|
"Notification stale",
|
|
source="mako",
|
|
source_generation=1,
|
|
notification_id=7,
|
|
live=False,
|
|
actions={},
|
|
)
|
|
|
|
gui = notification_presenter.NotificationListGUI.__new__(
|
|
notification_presenter.NotificationListGUI
|
|
)
|
|
gui._script = mock.Mock()
|
|
gui._presenter = self.presenter
|
|
gui._dismiss_button = _FakeButton()
|
|
gui._actions_box = _FakeBox()
|
|
gui._actions_status_label = _FakeLabel()
|
|
gui._model = _FakeListStore(
|
|
[
|
|
[entry.message, "now", entry],
|
|
[remaining.message, "later", remaining],
|
|
]
|
|
)
|
|
gui._selection = _FakeSelection(model=gui._model, selected_path=0)
|
|
gui._update_action_buttons = mock.Mock()
|
|
|
|
gui._dismiss_selected_notification()
|
|
|
|
self.assertEqual(self.monitor.dismissed, [6])
|
|
self.assertEqual(self.presenter._notifications, [remaining])
|
|
self.assertEqual(gui._model.rows, [[remaining.message, "later", remaining]])
|
|
gui._script.presentMessage.assert_called_with(messages.NOTIFICATION_DISMISSED)
|
|
|
|
def test_notification_list_dismiss_reselects_remaining_row_after_removing_last_selection(self):
|
|
remaining = self.presenter.save_notification(
|
|
"Notification stale",
|
|
source="mako",
|
|
source_generation=1,
|
|
notification_id=7,
|
|
live=False,
|
|
actions={},
|
|
)
|
|
entry = self.presenter.save_notification(
|
|
"Notification current",
|
|
source="mako",
|
|
source_generation=2,
|
|
notification_id=6,
|
|
live=True,
|
|
actions={"default": "View"},
|
|
)
|
|
|
|
gui = notification_presenter.NotificationListGUI.__new__(
|
|
notification_presenter.NotificationListGUI
|
|
)
|
|
gui._script = mock.Mock()
|
|
gui._presenter = self.presenter
|
|
gui._dismiss_button = _FakeButton()
|
|
gui._actions_box = _FakeBox()
|
|
gui._actions_status_label = _FakeLabel()
|
|
gui._model = _FakeListStore(
|
|
[
|
|
[remaining.message, "later", remaining],
|
|
[entry.message, "now", entry],
|
|
]
|
|
)
|
|
gui._selection = _FakeSelection(model=gui._model, selected_path=1)
|
|
gui._update_action_buttons = mock.Mock()
|
|
|
|
gui._dismiss_selected_notification()
|
|
|
|
self.assertEqual(gui._model.rows, [[remaining.message, "later", remaining]])
|
|
self.assertEqual(gui._selection.selected_path, 0)
|
|
|
|
def test_notification_list_builds_inline_action_buttons_in_reported_order(self):
|
|
entry = self.presenter.save_notification(
|
|
"Notification current",
|
|
source="mako",
|
|
source_generation=2,
|
|
notification_id=6,
|
|
live=True,
|
|
actions={"default": "View", "snooze": "Snooze"},
|
|
)
|
|
|
|
gui = notification_presenter.NotificationListGUI.__new__(
|
|
notification_presenter.NotificationListGUI
|
|
)
|
|
gui._script = mock.Mock()
|
|
gui._presenter = mock.Mock()
|
|
gui._presenter.can_control_entry.return_value = True
|
|
gui._presenter.get_actions_for_entry.return_value = {
|
|
"default": "View",
|
|
"snooze": "Snooze",
|
|
}
|
|
gui._presenter.invoke_action_for_entry.return_value = True
|
|
gui._dismiss_button = _FakeButton()
|
|
gui._actions_box = _FakeBox()
|
|
gui._actions_status_label = _FakeLabel()
|
|
gui._get_selected_entry = mock.Mock(return_value=entry)
|
|
|
|
with mock.patch.object(notification_presenter.Gtk, "Button", _FakeButton):
|
|
gui._update_action_buttons()
|
|
self.assertTrue(gui._dismiss_button.sensitive)
|
|
self.assertEqual(
|
|
[button.label for button in gui._actions_box.children],
|
|
["View", "Snooze"],
|
|
)
|
|
gui._actions_box.children[1].click()
|
|
|
|
gui._presenter.invoke_action_for_entry.assert_called_once_with(
|
|
gui._script,
|
|
entry,
|
|
"snooze",
|
|
)
|
|
|
|
def test_notification_list_clears_inline_actions_when_notification_is_unavailable(self):
|
|
entry = self.presenter.save_notification(
|
|
"Notification stale",
|
|
source="mako",
|
|
source_generation=1,
|
|
notification_id=7,
|
|
live=True,
|
|
actions={"default": "Open"},
|
|
)
|
|
|
|
gui = notification_presenter.NotificationListGUI.__new__(
|
|
notification_presenter.NotificationListGUI
|
|
)
|
|
gui._script = mock.Mock()
|
|
gui._presenter = mock.Mock()
|
|
gui._presenter.can_control_entry.return_value = False
|
|
gui._presenter.get_actions_for_entry.return_value = {}
|
|
gui._dismiss_button = _FakeButton()
|
|
gui._actions_box = _FakeBox()
|
|
gui._actions_box.children.append(_FakeButton("Old"))
|
|
gui._actions_status_label = _FakeLabel()
|
|
gui._get_selected_entry = mock.Mock(return_value=entry)
|
|
|
|
with mock.patch.object(notification_presenter.Gtk, "Button", _FakeButton):
|
|
gui._update_action_buttons()
|
|
|
|
self.assertFalse(gui._dismiss_button.sensitive)
|
|
self.assertEqual(gui._actions_box.children, [])
|
|
self.assertEqual(
|
|
gui._actions_status_label.text,
|
|
messages.NOTIFICATION_UNAVAILABLE,
|
|
)
|
|
self.assertTrue(gui._actions_status_label.visible)
|
|
|
|
def test_notification_list_shows_no_actions_state_for_live_notification_without_actions(self):
|
|
entry = self.presenter.save_notification(
|
|
"Notification current",
|
|
source="mako",
|
|
source_generation=2,
|
|
notification_id=8,
|
|
live=True,
|
|
actions={},
|
|
)
|
|
|
|
gui = notification_presenter.NotificationListGUI.__new__(
|
|
notification_presenter.NotificationListGUI
|
|
)
|
|
gui._script = mock.Mock()
|
|
gui._presenter = mock.Mock()
|
|
gui._presenter.can_control_entry.return_value = True
|
|
gui._presenter.get_actions_for_entry.return_value = {}
|
|
gui._dismiss_button = _FakeButton()
|
|
gui._actions_box = _FakeBox()
|
|
gui._actions_status_label = _FakeLabel()
|
|
gui._get_selected_entry = mock.Mock(return_value=entry)
|
|
|
|
with mock.patch.object(notification_presenter.Gtk, "Button", _FakeButton):
|
|
gui._update_action_buttons()
|
|
|
|
self.assertTrue(gui._dismiss_button.sensitive)
|
|
self.assertEqual(gui._actions_box.children, [])
|
|
self.assertEqual(
|
|
gui._actions_status_label.text,
|
|
messages.NOTIFICATION_NO_ACTIONS,
|
|
)
|
|
self.assertTrue(gui._actions_status_label.visible)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|