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()