diff --git a/tests/conftest.py b/tests/conftest.py index 3911ada..878ccc7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -95,3 +95,28 @@ def _stub_code_block_editor_dialog(monkeypatch): monkeypatch.setattr( markdown_editor, "CodeBlockEditorDialog", _TestCodeBlockEditorDialog ) + + +# --- Freeze Qt time helper (for alarm parsing tests) --- +@pytest.fixture +def freeze_qt_time(monkeypatch): + """Freeze QDateTime.currentDateTime/QTime.currentTime to midday today. + + This avoids flakiness when tests run close to midnight, so that + QTime.currentTime().addSecs(3600) is still the same calendar day. + """ + import bouquin.main_window as _mwmod + from PySide6.QtCore import QDate, QTime, QDateTime + + today = QDate.currentDate() + fixed_time = QTime(12, 0) + fixed_dt = QDateTime(today, fixed_time) + + # Patch the *imported* Qt symbols that main_window uses + monkeypatch.setattr( + _mwmod.QDateTime, "currentDateTime", staticmethod(lambda: QDateTime(fixed_dt)) + ) + monkeypatch.setattr( + _mwmod.QTime, "currentTime", staticmethod(lambda: QTime(fixed_time)) + ) + yield diff --git a/tests/test_code_block_editor_dialog.py b/tests/test_code_block_editor_dialog.py new file mode 100644 index 0000000..03e07c6 --- /dev/null +++ b/tests/test_code_block_editor_dialog.py @@ -0,0 +1,31 @@ +from PySide6.QtWidgets import QPushButton +from bouquin.code_block_editor_dialog import CodeBlockEditorDialog +from bouquin import strings + + +def _find_button_by_text(widget, text): + for btn in widget.findChildren(QPushButton): + if text.lower() in btn.text().lower(): + return btn + return None + + +def test_code_block_dialog_delete_flow(qtbot): + dlg = CodeBlockEditorDialog("print(1)", "python", allow_delete=True) + qtbot.addWidget(dlg) + delete_txt = strings._("delete_code_block") + delete_btn = _find_button_by_text(dlg, delete_txt) + assert delete_btn is not None + assert not dlg.was_deleted() + with qtbot.waitSignal(dlg.finished, timeout=2000): + delete_btn.click() + assert dlg.was_deleted() + + +def test_code_block_dialog_language_and_code(qtbot): + dlg = CodeBlockEditorDialog("x = 1", "not-a-lang", allow_delete=False) + qtbot.addWidget(dlg) + delete_txt = strings._("delete_code_block") + assert _find_button_by_text(dlg, delete_txt) is None + assert dlg.code() == "x = 1" + assert dlg.language() is None diff --git a/tests/test_reminders.py b/tests/test_reminders.py index c003d86..1a941c9 100644 --- a/tests/test_reminders.py +++ b/tests/test_reminders.py @@ -1,3 +1,5 @@ +import pytest + from unittest.mock import patch from bouquin.reminders import ( Reminder, @@ -6,12 +8,38 @@ from bouquin.reminders import ( UpcomingRemindersWidget, ManageRemindersDialog, ) -from PySide6.QtCore import QDate, QTime +from PySide6.QtCore import QDateTime, QDate, QTime from PySide6.QtWidgets import QDialog, QMessageBox, QWidget from datetime import date, timedelta +@pytest.fixture +def freeze_reminders_time(monkeypatch): + # Freeze 'now' used inside bouquin.reminders to 12:00 today + import bouquin.reminders as rem + + today = QDate.currentDate() + fixed_time = QTime(12, 0) + fixed_dt = QDateTime(today, fixed_time) + monkeypatch.setattr( + rem.QDateTime, "currentDateTime", staticmethod(lambda: QDateTime(fixed_dt)) + ) + yield + + +def _add_daily_reminder(db, text="Standup", time_str="23:59"): + r = Reminder( + id=None, + text=text, + time_str=time_str, + reminder_type=ReminderType.DAILY, + active=True, + ) + r.id = db.save_reminder(r) + return r + + def test_reminder_type_enum(app): """Test ReminderType enum values.""" assert ReminderType.ONCE is not None @@ -799,3 +827,104 @@ def test_edit_reminder_dialog(qtbot, fresh_db): # Verify fields are populated assert dlg.text_edit.text() == "Original text" assert dlg.time_edit.time().toString("HH:mm") == "14:30" + + +def test_upcoming_reminders_context_menu_shows( + qtbot, app, fresh_db, freeze_reminders_time, monkeypatch +): + from PySide6 import QtWidgets, QtGui + from PySide6.QtCore import QPoint + from bouquin.reminders import Reminder, ReminderType, UpcomingRemindersWidget + + # Add a future reminder for today + r = Reminder( + id=None, + text="Ping", + time_str="23:59", + reminder_type=ReminderType.DAILY, + active=True, + ) + r.id = fresh_db.save_reminder(r) + + w = UpcomingRemindersWidget(fresh_db) + qtbot.addWidget(w) + w.refresh() + + # Select first upcoming item so context menu code path runs + assert w.reminder_list.count() > 0 + w.reminder_list.setCurrentItem(w.reminder_list.item(0)) + + called = {"exec": False, "actions": []} + + class DummyAction: + def __init__(self, text, parent=None): + self._text = text + + class _Sig: + def connect(self, fn): + pass + + self.triggered = _Sig() + + class DummyMenu: + def __init__(self, parent=None): + pass + + def addAction(self, action): + called["actions"].append(getattr(action, "_text", str(action))) + + def exec(self, *_, **__): + called["exec"] = True + + # Patch the modules that the inline imports will read from + monkeypatch.setattr(QtWidgets, "QMenu", DummyMenu, raising=True) + monkeypatch.setattr(QtGui, "QAction", DummyAction, raising=True) + + # Invoke directly (normally via right-click) + w._show_reminder_context_menu(QPoint(5, 5)) + + assert called["exec"] is True + assert len(called["actions"]) >= 2 # at least Edit/Deactivate/Delete + + +def test_upcoming_reminders_delete_selected_dedupes( + qtbot, app, fresh_db, freeze_reminders_time, monkeypatch +): + from PySide6.QtWidgets import QMessageBox + from PySide6.QtCore import QItemSelectionModel + from bouquin.reminders import Reminder, ReminderType, UpcomingRemindersWidget + + r = Reminder( + id=None, + text="Duplicate target", + time_str="23:59", + reminder_type=ReminderType.DAILY, + active=True, + ) + r.id = fresh_db.save_reminder(r) + + w = UpcomingRemindersWidget(fresh_db) + qtbot.addWidget(w) + w.refresh() + + assert w.reminder_list.count() >= 2 # daily -> multiple upcoming occurrences + + # First selects & clears; second adds to selection + w.reminder_list.setCurrentRow(0, QItemSelectionModel.SelectionFlag.ClearAndSelect) + w.reminder_list.setCurrentRow(1, QItemSelectionModel.SelectionFlag.Select) + + deleted_ids = [] + + def fake_delete(rem_id): + deleted_ids.append(rem_id) + + # Auto-confirm deletion + monkeypatch.setattr( + QMessageBox, "question", staticmethod(lambda *a, **k: QMessageBox.Yes) + ) + monkeypatch.setattr(fresh_db, "delete_reminder", fake_delete) + + w._delete_selected_reminders() + + # Should de-duplicate to a single DB delete call + assert deleted_ids == [r.id]