from unittest.mock import patch from bouquin.reminders import ( Reminder, ReminderType, ReminderDialog, UpcomingRemindersWidget, ManageRemindersDialog, ) from PySide6.QtCore import QDate, QTime from PySide6.QtWidgets import QDialog, QMessageBox, QWidget from datetime import date, timedelta def test_reminder_type_enum(app): """Test ReminderType enum values.""" assert ReminderType.ONCE is not None assert ReminderType.DAILY is not None assert ReminderType.WEEKDAYS is not None assert ReminderType.WEEKLY is not None def test_reminder_dataclass_creation(app): """Test creating a Reminder instance.""" reminder = Reminder( id=1, text="Test reminder", time_str="10:30", reminder_type=ReminderType.DAILY, weekday=None, active=True, date_iso=None, ) assert reminder.id == 1 assert reminder.text == "Test reminder" assert reminder.time_str == "10:30" assert reminder.reminder_type == ReminderType.DAILY assert reminder.active is True def test_reminder_dialog_init_new(qtbot, app, fresh_db): """Test ReminderDialog initialization for new reminder.""" dialog = ReminderDialog(fresh_db) qtbot.addWidget(dialog) assert dialog._db is fresh_db assert dialog._reminder is None assert dialog.text_edit.text() == "" def test_reminder_dialog_init_existing(qtbot, app, fresh_db): """Test ReminderDialog initialization with existing reminder.""" reminder = Reminder( id=1, text="Existing reminder", time_str="14:30", reminder_type=ReminderType.WEEKLY, weekday=2, active=True, ) dialog = ReminderDialog(fresh_db, reminder=reminder) qtbot.addWidget(dialog) assert dialog.text_edit.text() == "Existing reminder" assert dialog.time_edit.time().hour() == 14 assert dialog.time_edit.time().minute() == 30 def test_reminder_dialog_type_changed(qtbot, app, fresh_db): """Test that weekday combo visibility changes with type.""" dialog = ReminderDialog(fresh_db) qtbot.addWidget(dialog) dialog.show() # Show the dialog so child widgets can be visible # Find weekly type in combo for i in range(dialog.type_combo.count()): if dialog.type_combo.itemData(i) == ReminderType.WEEKLY: dialog.type_combo.setCurrentIndex(i) break qtbot.wait(10) # Wait for Qt event processing assert dialog.weekday_combo.isVisible() is True # Switch to daily for i in range(dialog.type_combo.count()): if dialog.type_combo.itemData(i) == ReminderType.DAILY: dialog.type_combo.setCurrentIndex(i) break qtbot.wait(10) # Wait for Qt event processing assert dialog.weekday_combo.isVisible() is False def test_reminder_dialog_get_reminder_once(qtbot, app, fresh_db): """Test getting reminder with ONCE type.""" dialog = ReminderDialog(fresh_db) qtbot.addWidget(dialog) dialog.text_edit.setText("Test task") dialog.time_edit.setTime(QTime(10, 30)) # Set to ONCE type for i in range(dialog.type_combo.count()): if dialog.type_combo.itemData(i) == ReminderType.ONCE: dialog.type_combo.setCurrentIndex(i) break reminder = dialog.get_reminder() assert reminder.text == "Test task" assert reminder.time_str == "10:30" assert reminder.reminder_type == ReminderType.ONCE assert reminder.date_iso is not None def test_reminder_dialog_get_reminder_weekly(qtbot, app, fresh_db): """Test getting reminder with WEEKLY type.""" dialog = ReminderDialog(fresh_db) qtbot.addWidget(dialog) dialog.text_edit.setText("Weekly meeting") dialog.time_edit.setTime(QTime(15, 0)) # Set to WEEKLY type for i in range(dialog.type_combo.count()): if dialog.type_combo.itemData(i) == ReminderType.WEEKLY: dialog.type_combo.setCurrentIndex(i) break dialog.weekday_combo.setCurrentIndex(1) # Tuesday reminder = dialog.get_reminder() assert reminder.text == "Weekly meeting" assert reminder.reminder_type == ReminderType.WEEKLY assert reminder.weekday == 1 def test_upcoming_reminders_widget_init(qtbot, app, fresh_db): """Test UpcomingRemindersWidget initialization.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) assert widget._db is fresh_db assert widget.body.isVisible() is False def test_upcoming_reminders_widget_toggle(qtbot, app, fresh_db): """Test toggling reminder list visibility.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) widget.show() # Show the widget so child widgets can be visible # Initially hidden assert widget.body.isVisible() is False # Click toggle widget.toggle_btn.click() qtbot.wait(10) # Wait for Qt event processing assert widget.body.isVisible() is True def test_upcoming_reminders_widget_should_fire_on_date_once(qtbot, app, fresh_db): """Test should_fire_on_date for ONCE type.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) reminder = Reminder( id=1, text="Test", time_str="10:00", reminder_type=ReminderType.ONCE, date_iso="2024-01-15", ) assert widget._should_fire_on_date(reminder, QDate(2024, 1, 15)) is True assert widget._should_fire_on_date(reminder, QDate(2024, 1, 16)) is False def test_upcoming_reminders_widget_should_fire_on_date_daily(qtbot, app, fresh_db): """Test should_fire_on_date for DAILY type.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) reminder = Reminder( id=1, text="Test", time_str="10:00", reminder_type=ReminderType.DAILY, ) # Should fire every day assert widget._should_fire_on_date(reminder, QDate(2024, 1, 15)) is True assert widget._should_fire_on_date(reminder, QDate(2024, 1, 16)) is True def test_upcoming_reminders_widget_should_fire_on_date_weekdays(qtbot, app, fresh_db): """Test should_fire_on_date for WEEKDAYS type.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) reminder = Reminder( id=1, text="Test", time_str="10:00", reminder_type=ReminderType.WEEKDAYS, ) # Monday (dayOfWeek = 1) assert widget._should_fire_on_date(reminder, QDate(2024, 1, 15)) is True # Friday (dayOfWeek = 5) assert widget._should_fire_on_date(reminder, QDate(2024, 1, 19)) is True # Saturday (dayOfWeek = 6) assert widget._should_fire_on_date(reminder, QDate(2024, 1, 20)) is False # Sunday (dayOfWeek = 7) assert widget._should_fire_on_date(reminder, QDate(2024, 1, 21)) is False def test_upcoming_reminders_widget_should_fire_on_date_weekly(qtbot, app, fresh_db): """Test should_fire_on_date for WEEKLY type.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) # Fire on Wednesday (weekday = 2) reminder = Reminder( id=1, text="Test", time_str="10:00", reminder_type=ReminderType.WEEKLY, weekday=2, ) # Wednesday (dayOfWeek = 3, so weekday = 2) assert widget._should_fire_on_date(reminder, QDate(2024, 1, 17)) is True # Thursday (dayOfWeek = 4, so weekday = 3) assert widget._should_fire_on_date(reminder, QDate(2024, 1, 18)) is False def test_upcoming_reminders_widget_refresh_no_db(qtbot, app): """Test refresh with no database connection.""" widget = UpcomingRemindersWidget(None) qtbot.addWidget(widget) # Should not crash widget.refresh() def test_upcoming_reminders_widget_refresh_with_reminders(qtbot, app, fresh_db): """Test refresh displays reminders.""" # Add a reminder to the database reminder = Reminder( id=None, text="Test reminder", time_str="23:59", # Late time so it's in the future reminder_type=ReminderType.DAILY, active=True, ) fresh_db.save_reminder(reminder) widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) widget.refresh() # Should have at least one item (or "No upcoming reminders") assert widget.reminder_list.count() > 0 def test_upcoming_reminders_widget_add_reminder(qtbot, app, fresh_db): """Test adding a reminder through the widget.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) with patch.object(ReminderDialog, "exec", return_value=QDialog.Accepted): with patch.object(ReminderDialog, "get_reminder") as mock_get: mock_get.return_value = Reminder( id=None, text="New reminder", time_str="10:00", reminder_type=ReminderType.DAILY, ) widget._add_reminder() # Reminder should be saved reminders = fresh_db.get_all_reminders() assert len(reminders) > 0 def test_upcoming_reminders_widget_edit_reminder(qtbot, app, fresh_db): """Test editing a reminder through the widget.""" # Add a reminder first reminder = Reminder( id=None, text="Original", time_str="10:00", reminder_type=ReminderType.DAILY, active=True, ) fresh_db.save_reminder(reminder) widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) widget.refresh() # Get the list item if widget.reminder_list.count() > 0: item = widget.reminder_list.item(0) with patch.object(ReminderDialog, "exec", return_value=QDialog.Accepted): with patch.object(ReminderDialog, "get_reminder") as mock_get: updated = Reminder( id=1, text="Updated", time_str="11:00", reminder_type=ReminderType.DAILY, ) mock_get.return_value = updated widget._edit_reminder(item) def test_upcoming_reminders_widget_delete_selected_single(qtbot, app, fresh_db): """Test deleting a single selected reminder.""" # Add a reminder reminder = Reminder( id=None, text="To delete", time_str="10:00", reminder_type=ReminderType.DAILY, active=True, ) fresh_db.save_reminder(reminder) widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) widget.refresh() if widget.reminder_list.count() > 0: widget.reminder_list.setCurrentRow(0) with patch.object(QMessageBox, "question", return_value=QMessageBox.Yes): widget._delete_selected_reminders() def test_upcoming_reminders_widget_delete_selected_multiple(qtbot, app, fresh_db): """Test deleting multiple selected reminders.""" # Add multiple reminders for i in range(3): reminder = Reminder( id=None, text=f"Reminder {i}", time_str="23:59", reminder_type=ReminderType.DAILY, active=True, ) fresh_db.save_reminder(reminder) widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) widget.refresh() # Select all items for i in range(widget.reminder_list.count()): widget.reminder_list.item(i).setSelected(True) with patch.object(QMessageBox, "question", return_value=QMessageBox.Yes): widget._delete_selected_reminders() def test_upcoming_reminders_widget_check_reminders_no_db(qtbot, app): """Test check_reminders with no database.""" widget = UpcomingRemindersWidget(None) qtbot.addWidget(widget) # Should not crash widget._check_reminders() def test_upcoming_reminders_widget_start_regular_timer(qtbot, app, fresh_db): """Test starting the regular check timer.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) widget._start_regular_timer() # Timer should be running assert widget._check_timer.isActive() def test_manage_reminders_dialog_init(qtbot, app, fresh_db): """Test ManageRemindersDialog initialization.""" dialog = ManageRemindersDialog(fresh_db) qtbot.addWidget(dialog) assert dialog._db is fresh_db assert dialog.table is not None def test_manage_reminders_dialog_load_reminders(qtbot, app, fresh_db): """Test loading reminders into the table.""" # Add some reminders for i in range(3): reminder = Reminder( id=None, text=f"Reminder {i}", time_str="10:00", reminder_type=ReminderType.DAILY, active=True, ) fresh_db.save_reminder(reminder) dialog = ManageRemindersDialog(fresh_db) qtbot.addWidget(dialog) assert dialog.table.rowCount() == 3 def test_manage_reminders_dialog_load_reminders_no_db(qtbot, app): """Test loading reminders with no database.""" dialog = ManageRemindersDialog(None) qtbot.addWidget(dialog) # Should not crash dialog._load_reminders() def test_manage_reminders_dialog_add_reminder(qtbot, app, fresh_db): """Test adding a reminder through the manage dialog.""" dialog = ManageRemindersDialog(fresh_db) qtbot.addWidget(dialog) initial_count = dialog.table.rowCount() with patch.object(ReminderDialog, "exec", return_value=QDialog.Accepted): with patch.object(ReminderDialog, "get_reminder") as mock_get: mock_get.return_value = Reminder( id=None, text="New", time_str="10:00", reminder_type=ReminderType.DAILY, ) dialog._add_reminder() # Table should have one more row assert dialog.table.rowCount() == initial_count + 1 def test_manage_reminders_dialog_edit_reminder(qtbot, app, fresh_db): """Test editing a reminder through the manage dialog.""" reminder = Reminder( id=None, text="Original", time_str="10:00", reminder_type=ReminderType.DAILY, active=True, ) fresh_db.save_reminder(reminder) dialog = ManageRemindersDialog(fresh_db) qtbot.addWidget(dialog) with patch.object(ReminderDialog, "exec", return_value=QDialog.Accepted): with patch.object(ReminderDialog, "get_reminder") as mock_get: mock_get.return_value = Reminder( id=1, text="Updated", time_str="11:00", reminder_type=ReminderType.DAILY, ) dialog._edit_reminder(reminder) def test_manage_reminders_dialog_delete_reminder(qtbot, app, fresh_db): """Test deleting a reminder through the manage dialog.""" reminder = Reminder( id=None, text="To delete", time_str="10:00", reminder_type=ReminderType.DAILY, active=True, ) fresh_db.save_reminder(reminder) saved_reminders = fresh_db.get_all_reminders() reminder_to_delete = saved_reminders[0] dialog = ManageRemindersDialog(fresh_db) qtbot.addWidget(dialog) initial_count = dialog.table.rowCount() with patch.object(QMessageBox, "question", return_value=QMessageBox.Yes): dialog._delete_reminder(reminder_to_delete) # Table should have one fewer row assert dialog.table.rowCount() == initial_count - 1 def test_manage_reminders_dialog_delete_reminder_declined(qtbot, app, fresh_db): """Test declining to delete a reminder.""" reminder = Reminder( id=None, text="Keep me", time_str="10:00", reminder_type=ReminderType.DAILY, active=True, ) fresh_db.save_reminder(reminder) saved_reminders = fresh_db.get_all_reminders() reminder_to_keep = saved_reminders[0] dialog = ManageRemindersDialog(fresh_db) qtbot.addWidget(dialog) initial_count = dialog.table.rowCount() with patch.object(QMessageBox, "question", return_value=QMessageBox.No): dialog._delete_reminder(reminder_to_keep) # Table should have same number of rows assert dialog.table.rowCount() == initial_count def test_manage_reminders_dialog_weekly_reminder_display(qtbot, app, fresh_db): """Test that weekly reminders display the day name.""" reminder = Reminder( id=None, text="Weekly", time_str="10:00", reminder_type=ReminderType.WEEKLY, weekday=2, # Wednesday active=True, ) fresh_db.save_reminder(reminder) dialog = ManageRemindersDialog(fresh_db) qtbot.addWidget(dialog) # Check that the type column shows the day type_item = dialog.table.item(0, 2) assert "Wed" in type_item.text() def test_reminder_dialog_accept(qtbot, app, fresh_db): """Test accepting the reminder dialog.""" dialog = ReminderDialog(fresh_db) qtbot.addWidget(dialog) dialog.text_edit.setText("Test") dialog.accept() def test_reminder_dialog_reject(qtbot, app, fresh_db): """Test rejecting the reminder dialog.""" dialog = ReminderDialog(fresh_db) qtbot.addWidget(dialog) dialog.reject() def test_upcoming_reminders_widget_signal_emitted(qtbot, app, fresh_db): """Test that reminderTriggered signal is emitted.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) signal_received = [] widget.reminderTriggered.connect(lambda text: signal_received.append(text)) # Manually emit for testing widget.reminderTriggered.emit("Test reminder") assert len(signal_received) == 1 assert signal_received[0] == "Test reminder" def test_upcoming_reminders_widget_no_upcoming_message(qtbot, app, fresh_db): """Test that 'No upcoming reminders' message is shown when appropriate.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) widget.refresh() # Should show message when no reminders if widget.reminder_list.count() > 0: item = widget.reminder_list.item(0) if "No upcoming" in item.text(): assert True def test_upcoming_reminders_widget_manage_button(qtbot, app, fresh_db): """Test clicking the manage button.""" widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) with patch.object(ManageRemindersDialog, "exec"): widget._manage_reminders() def test_reminder_dialog_time_format(qtbot, app, fresh_db): """Test that time is formatted correctly.""" dialog = ReminderDialog(fresh_db) qtbot.addWidget(dialog) dialog.time_edit.setTime(QTime(9, 5)) reminder = dialog.get_reminder() assert reminder.time_str == "09:05" def test_upcoming_reminders_widget_past_reminders_filtered(qtbot, app, fresh_db): """Test that past reminders are not shown in upcoming list.""" # Create a reminder that's in the past reminder = Reminder( id=None, text="Past reminder", time_str="00:01", # Very early morning reminder_type=ReminderType.DAILY, active=True, ) fresh_db.save_reminder(reminder) widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) # Current time should be past 00:01 from PySide6.QtCore import QTime if QTime.currentTime().hour() > 0: widget.refresh() # The past reminder for today should be filtered out # but tomorrow's occurrence should be shown def test_reminder_with_inactive_status(qtbot, app, fresh_db): """Test that inactive reminders are not displayed.""" reminder = Reminder( id=None, text="Inactive", time_str="23:59", reminder_type=ReminderType.DAILY, active=False, ) fresh_db.save_reminder(reminder) widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(widget) widget.refresh() # Should not show inactive reminder for i in range(widget.reminder_list.count()): item = widget.reminder_list.item(i) assert "Inactive" not in item.text() or "No upcoming" in item.text() def test_reminder_triggers_and_deactivates(qtbot, fresh_db): """Test that ONCE reminders deactivate after firing.""" # Add a ONCE reminder for right now now = QTime.currentTime() hour = now.hour() minute = now.minute() reminder = Reminder( id=None, text="Test once reminder", reminder_type=ReminderType.ONCE, time_str=f"{hour:02d}:{minute:02d}", date_iso=date.today().isoformat(), active=True, ) reminder_id = fresh_db.save_reminder(reminder) reminders_widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(reminders_widget) # Set up signal spy triggered_texts = [] reminders_widget.reminderTriggered.connect( lambda text: triggered_texts.append(text) ) # Trigger the check reminders_widget._check_reminders() # Verify reminder was triggered assert len(triggered_texts) > 0 assert "Test once reminder" in triggered_texts # Verify reminder was deactivated reminders = fresh_db.get_all_reminders() deactivated = [r for r in reminders if r.id == reminder_id][0] assert deactivated.active is False def test_reminder_not_active_skipped(qtbot, fresh_db): """Test that inactive reminders are not triggered.""" now = QTime.currentTime() hour = now.hour() minute = now.minute() reminder = Reminder( id=None, text="Inactive reminder", reminder_type=ReminderType.ONCE, time_str=f"{hour:02d}:{minute:02d}", date_iso=date.today().isoformat(), active=False, # Not active ) fresh_db.save_reminder(reminder) reminders_widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(reminders_widget) # Set up signal spy triggered_texts = [] reminders_widget.reminderTriggered.connect( lambda text: triggered_texts.append(text) ) # Trigger the check reminders_widget._check_reminders() # Should not trigger inactive reminder assert len(triggered_texts) == 0 def test_reminder_not_today_skipped(qtbot, fresh_db): """Test that reminders not scheduled for today are skipped.""" now = QTime.currentTime() hour = now.hour() minute = now.minute() # Schedule for tomorrow tomorrow = date.today() + timedelta(days=1) reminder = Reminder( id=None, text="Tomorrow's reminder", reminder_type=ReminderType.ONCE, time_str=f"{hour:02d}:{minute:02d}", date_iso=tomorrow.isoformat(), active=True, ) fresh_db.save_reminder(reminder) reminders_widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(reminders_widget) # Set up signal spy triggered_texts = [] reminders_widget.reminderTriggered.connect( lambda text: triggered_texts.append(text) ) # Trigger the check reminders_widget._check_reminders() # Should not trigger tomorrow's reminder assert len(triggered_texts) == 0 def test_reminder_context_menu_no_selection(qtbot, fresh_db): """Test context menu with no selection returns early.""" reminders_widget = UpcomingRemindersWidget(fresh_db) qtbot.addWidget(reminders_widget) # Clear selection reminders_widget.reminder_list.clearSelection() # Show context menu - should return early reminders_widget._show_reminder_context_menu(reminders_widget.reminder_list.pos()) def test_edit_reminder_dialog(qtbot, fresh_db): """Test editing a reminder through the dialog.""" reminder = Reminder( id=None, text="Original text", reminder_type=ReminderType.DAILY, time_str="14:30", date_iso=None, active=True, ) fresh_db.save_reminder(reminder) widget = QWidget() # Create edit dialog reminder_obj = fresh_db.get_all_reminders()[0] dlg = ReminderDialog(fresh_db, widget, reminder=reminder_obj) qtbot.addWidget(dlg) # Verify fields are populated assert dlg.text_edit.text() == "Original text" assert dlg.time_edit.time().toString("HH:mm") == "14:30"