Many changes and new features:
All checks were successful
CI / test (push) Successful in 5m17s
Lint / test (push) Successful in 32s
Trivy / test (push) Successful in 25s

* Make reminders be its own dataset rather than tied to current string.
 * Add support for repeated reminders
 * Make reminders be a feature that can be turned on and off
 * Add syntax highlighting for code blocks (right-click to set it)
 * Add a Pomodoro-style timer for measuring time spent on a task (stopping the timer offers to log it to Time Log)
 * Add ability to create markdown tables. Right-click to edit the table in a friendlier table dialog
This commit is contained in:
Miguel Jacq 2025-11-25 14:52:26 +11:00
parent 26737fbfb2
commit e0169db52a
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
28 changed files with 4191 additions and 17 deletions

657
tests/test_reminders.py Normal file
View file

@ -0,0 +1,657 @@
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
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()