More tests
Some checks failed
CI / test (push) Has been cancelled
Lint / test (push) Successful in 32s
Trivy / test (push) Successful in 25s

This commit is contained in:
Miguel Jacq 2025-11-26 17:12:58 +11:00
parent cb78d9f783
commit 9435800910
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
12 changed files with 1187 additions and 35 deletions

View file

@ -536,3 +536,69 @@ def test_db_gather_stats_exception_in_dates_with_content(fresh_db, monkeypatch):
# Should default to 0 when exception occurs
assert pages_with_content == 0
def test_delete_version(fresh_db):
"""Test deleting a specific version by version_id."""
d = date.today().isoformat()
# Create multiple versions
vid1, _ = fresh_db.save_new_version(d, "version 1", "note1")
vid2, _ = fresh_db.save_new_version(d, "version 2", "note2")
vid3, _ = fresh_db.save_new_version(d, "version 3", "note3")
# Verify all versions exist
versions = fresh_db.list_versions(d)
assert len(versions) == 3
# Delete the second version
fresh_db.delete_version(version_id=vid2)
# Verify it's deleted
versions_after = fresh_db.list_versions(d)
assert len(versions_after) == 2
# Make sure the deleted version is not in the list
version_ids = [v["id"] for v in versions_after]
assert vid2 not in version_ids
assert vid1 in version_ids
assert vid3 in version_ids
def test_update_reminder_active(fresh_db):
"""Test updating the active status of a reminder."""
from bouquin.reminders import Reminder, ReminderType
# Create a reminder object
reminder = Reminder(
id=None,
text="Test reminder",
reminder_type=ReminderType.ONCE,
time_str="14:30",
date_iso=date.today().isoformat(),
active=True,
)
# Save it
reminder_id = fresh_db.save_reminder(reminder)
# Verify it's active
reminders = fresh_db.get_all_reminders()
active_reminder = [r for r in reminders if r.id == reminder_id][0]
assert active_reminder.active is True
# Deactivate it
fresh_db.update_reminder_active(reminder_id, False)
# Verify it's inactive
reminders = fresh_db.get_all_reminders()
inactive_reminder = [r for r in reminders if r.id == reminder_id][0]
assert inactive_reminder.active is False
# Reactivate it
fresh_db.update_reminder_active(reminder_id, True)
# Verify it's active again
reminders = fresh_db.get_all_reminders()
reactivated_reminder = [r for r in reminders if r.id == reminder_id][0]
assert reactivated_reminder.active is True

View file

@ -167,3 +167,145 @@ def test_revert_exception_shows_message(qtbot, fresh_db, monkeypatch):
# Should show the critical box, which our timer will accept; _revert returns.
dlg._revert()
def test_delete_version_from_history(qtbot, fresh_db):
"""Test deleting a version through the history dialog."""
d = "2001-01-01"
# Create multiple versions
vid1, _ = fresh_db.save_new_version(d, "v1", "first")
vid2, _ = fresh_db.save_new_version(d, "v2", "second")
vid3, _ = fresh_db.save_new_version(d, "v3", "third")
w = QWidget()
dlg = HistoryDialog(fresh_db, d, parent=w)
qtbot.addWidget(dlg)
dlg.show()
# Verify we have 3 versions
assert dlg.list.count() == 3
# Select the first version (oldest, not current)
dlg.list.setCurrentRow(2) # Last row is oldest version
# Call _delete
dlg._delete()
# Verify the version was deleted
assert dlg.list.count() == 2
# Verify from DB
versions = fresh_db.list_versions(d)
assert len(versions) == 2
def test_delete_current_version_returns_early(qtbot, fresh_db):
"""Test that deleting the current version returns early without deleting."""
d = "2001-01-02"
# Create versions
vid1, _ = fresh_db.save_new_version(d, "v1", "first")
vid2, _ = fresh_db.save_new_version(d, "v2", "second")
w = QWidget()
dlg = HistoryDialog(fresh_db, d, parent=w)
qtbot.addWidget(dlg)
dlg.show()
# Find and select the current version
for i in range(dlg.list.count()):
item = dlg.list.item(i)
if item.data(Qt.UserRole) == dlg._current_id:
dlg.list.setCurrentItem(item)
break
# Try to delete - should return early
dlg._delete()
# Verify nothing was deleted
versions = fresh_db.list_versions(d)
assert len(versions) == 2
def test_delete_version_with_error(qtbot, fresh_db, monkeypatch):
"""Test that delete version error shows a message box."""
d = "2001-01-03"
# Create versions
vid1, _ = fresh_db.save_new_version(d, "v1", "first")
vid2, _ = fresh_db.save_new_version(d, "v2", "second")
w = QWidget()
dlg = HistoryDialog(fresh_db, d, parent=w)
qtbot.addWidget(dlg)
dlg.show()
# Select a non-current version
for i in range(dlg.list.count()):
item = dlg.list.item(i)
if item.data(Qt.UserRole) != dlg._current_id:
dlg.list.setCurrentItem(item)
break
# Make delete_version raise an error
def boom(*args, **kwargs):
raise RuntimeError("Delete failed")
monkeypatch.setattr(dlg._db, "delete_version", boom)
# Set up auto-closer for message box
def make_closer(max_tries=50, interval_ms=10):
tries = {"n": 0}
def closer():
tries["n"] += 1
w = QApplication.activeModalWidget()
if isinstance(w, QMessageBox):
ok = w.button(QMessageBox.Ok)
if ok is not None:
ok.click()
else:
w.accept()
elif tries["n"] < max_tries:
QTimer.singleShot(interval_ms, closer)
return closer
QTimer.singleShot(0, make_closer())
# Call delete - should show error message
dlg._delete()
def test_delete_multiple_versions(qtbot, fresh_db):
"""Test deleting multiple versions at once."""
d = "2001-01-04"
# Create multiple versions
vid1, _ = fresh_db.save_new_version(d, "v1", "first")
vid2, _ = fresh_db.save_new_version(d, "v2", "second")
vid3, _ = fresh_db.save_new_version(d, "v3", "third")
vid4, _ = fresh_db.save_new_version(d, "v4", "fourth")
w = QWidget()
dlg = HistoryDialog(fresh_db, d, parent=w)
qtbot.addWidget(dlg)
dlg.show()
# Select multiple non-current items
selected_count = 0
for i in range(dlg.list.count()):
item = dlg.list.item(i)
if item.data(Qt.UserRole) != dlg._current_id:
item.setSelected(True)
selected_count += 1
if selected_count >= 2: # Select 2 items
break
# Delete them
dlg._delete()
# Verify versions were deleted (should have current + 1 remaining)
versions = fresh_db.list_versions(d)
assert len(versions) == 2 # Current + 1 that wasn't deleted

View file

@ -1,6 +1,7 @@
import pytest
import importlib.metadata
from datetime import date, timedelta
from pathlib import Path
import bouquin.main_window as mwmod
@ -2134,3 +2135,352 @@ def test_calendar_date_selection(qtbot, app, tmp_path):
# The window should load that date
assert test_date.toString("yyyy-MM-dd") in str(w._current_date_iso())
def test_main_window_without_reminders(qtbot, app, tmp_db_cfg):
"""Test main window when reminders feature is disabled."""
s = get_settings()
s.setValue("db/default_db", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key)
s.setValue("ui/idle_minutes", 0)
s.setValue("ui/theme", "light")
s.setValue("ui/move_todos", True)
s.setValue("ui/tags", True)
s.setValue("ui/time_log", True)
s.setValue("ui/reminders", False) # Disabled
s.setValue("ui/locale", "en")
s.setValue("ui/font_size", 11)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
window = MainWindow(themes=themes)
qtbot.addWidget(window)
window.show()
# Verify reminders widget is hidden
assert window.upcoming_reminders.isHidden()
assert not window.toolBar.actAlarm.isVisible()
def test_main_window_without_time_log(qtbot, app, tmp_db_cfg):
"""Test main window when time_log feature is disabled."""
s = get_settings()
s.setValue("db/default_db", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key)
s.setValue("ui/idle_minutes", 0)
s.setValue("ui/theme", "light")
s.setValue("ui/move_todos", True)
s.setValue("ui/tags", True)
s.setValue("ui/time_log", False) # Disabled
s.setValue("ui/reminders", True)
s.setValue("ui/locale", "en")
s.setValue("ui/font_size", 11)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
window = MainWindow(themes=themes)
qtbot.addWidget(window)
window.show()
# Verify time_log widget is hidden
assert window.time_log.isHidden()
assert not window.toolBar.actTimer.isVisible()
def test_main_window_without_tags(qtbot, app, tmp_db_cfg):
"""Test main window when tags feature is disabled."""
s = get_settings()
s.setValue("db/default_db", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key)
s.setValue("ui/idle_minutes", 0)
s.setValue("ui/theme", "light")
s.setValue("ui/move_todos", True)
s.setValue("ui/tags", False) # Disabled
s.setValue("ui/time_log", True)
s.setValue("ui/reminders", True)
s.setValue("ui/locale", "en")
s.setValue("ui/font_size", 11)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
window = MainWindow(themes=themes)
qtbot.addWidget(window)
window.show()
# Verify tags widget is hidden
assert window.tags.isHidden()
def test_close_current_tab(qtbot, app, tmp_db_cfg, fresh_db):
"""Test closing the current tab via _close_current_tab."""
s = get_settings()
s.setValue("db/default_db", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key)
s.setValue("ui/idle_minutes", 0)
s.setValue("ui/theme", "light")
s.setValue("ui/move_todos", True)
s.setValue("ui/tags", True)
s.setValue("ui/time_log", True)
s.setValue("ui/reminders", True)
s.setValue("ui/locale", "en")
s.setValue("ui/font_size", 11)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
window = MainWindow(themes=themes)
qtbot.addWidget(window)
window.show()
# Open multiple tabs
today = date.today().isoformat()
tomorrow = (date.today() + timedelta(days=1)).isoformat()
window._open_date_in_tab(QDate.fromString(today, "yyyy-MM-dd"))
window._open_date_in_tab(QDate.fromString(tomorrow, "yyyy-MM-dd"))
initial_count = window.tab_widget.count()
assert initial_count >= 2
# Close current tab
window._close_current_tab()
# Verify tab was closed
assert window.tab_widget.count() == initial_count - 1
def test_table_insertion(qtbot, app, tmp_db_cfg, fresh_db):
"""Test inserting a table template."""
s = get_settings()
s.setValue("db/default_db", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key)
s.setValue("ui/idle_minutes", 0)
s.setValue("ui/theme", "light")
s.setValue("ui/move_todos", True)
s.setValue("ui/tags", True)
s.setValue("ui/time_log", True)
s.setValue("ui/reminders", True)
s.setValue("ui/locale", "en")
s.setValue("ui/font_size", 11)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
window = MainWindow(themes=themes)
qtbot.addWidget(window)
window.show()
# Open a date
today = date.today().isoformat()
window._open_date_in_tab(QDate.fromString(today, "yyyy-MM-dd"))
# Ensure we have an editor
editor = window.editor
assert editor is not None
# Insert table
window._on_table_requested()
# Verify table was inserted
text = editor.toPlainText()
assert "Column 1" in text
assert "Column 2" in text
assert "Column 3" in text
assert "---" in text
def test_parse_today_alarms(qtbot, app, tmp_db_cfg, fresh_db):
"""Test parsing inline alarms from markdown (⏰ HH:MM format)."""
from PySide6.QtCore import QTime
s = get_settings()
s.setValue("db/default_db", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key)
s.setValue("ui/idle_minutes", 0)
s.setValue("ui/theme", "light")
s.setValue("ui/move_todos", True)
s.setValue("ui/tags", True)
s.setValue("ui/time_log", True)
s.setValue("ui/reminders", True)
s.setValue("ui/locale", "en")
s.setValue("ui/font_size", 11)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
window = MainWindow(themes=themes)
qtbot.addWidget(window)
window.show()
# Open today's date
today_qdate = QDate.currentDate()
window._open_date_in_tab(today_qdate)
# Set content with a future alarm
future_time = QTime.currentTime().addSecs(3600) # 1 hour from now
alarm_text = f"Do something ⏰ {future_time.hour():02d}:{future_time.minute():02d}"
# Set the editor's current_date attribute
window.editor.current_date = today_qdate
window.editor.setPlainText(alarm_text)
# Clear any existing timers
window._reminder_timers = []
# Trigger alarm parsing
window._rebuild_reminders_for_today()
# Verify timer was created (not DB reminder)
assert len(window._reminder_timers) > 0
def test_parse_today_alarms_invalid_time(qtbot, app, tmp_db_cfg, fresh_db):
"""Test that invalid time formats are skipped."""
s = get_settings()
s.setValue("db/default_db", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key)
s.setValue("ui/idle_minutes", 0)
s.setValue("ui/theme", "light")
s.setValue("ui/move_todos", True)
s.setValue("ui/tags", True)
s.setValue("ui/time_log", True)
s.setValue("ui/reminders", True)
s.setValue("ui/locale", "en")
s.setValue("ui/font_size", 11)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
window = MainWindow(themes=themes)
qtbot.addWidget(window)
window.show()
# Open today's date
today_qdate = QDate.currentDate()
window._open_date_in_tab(today_qdate)
# Set content with invalid time
alarm_text = "Do something ⏰ 25:99" # Invalid time
window.editor.current_date = today_qdate
window.editor.setPlainText(alarm_text)
# Clear any existing timers
window._reminder_timers = []
# Trigger alarm parsing - should not crash
window._rebuild_reminders_for_today()
# No timer should be created for invalid time
assert len(window._reminder_timers) == 0
def test_parse_today_alarms_past_time(qtbot, app, tmp_db_cfg, fresh_db):
"""Test that past alarms are skipped."""
from PySide6.QtCore import QTime
s = get_settings()
s.setValue("db/default_db", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key)
s.setValue("ui/idle_minutes", 0)
s.setValue("ui/theme", "light")
s.setValue("ui/move_todos", True)
s.setValue("ui/tags", True)
s.setValue("ui/time_log", True)
s.setValue("ui/reminders", True)
s.setValue("ui/locale", "en")
s.setValue("ui/font_size", 11)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
window = MainWindow(themes=themes)
qtbot.addWidget(window)
window.show()
# Open today's date
today_qdate = QDate.currentDate()
window._open_date_in_tab(today_qdate)
# Set content with past time
past_time = QTime.currentTime().addSecs(-3600) # 1 hour ago
alarm_text = f"Do something ⏰ {past_time.hour():02d}:{past_time.minute():02d}"
window.editor.current_date = today_qdate
window.editor.setPlainText(alarm_text)
# Clear any existing timers
window._reminder_timers = []
# Trigger alarm parsing
window._rebuild_reminders_for_today()
# Past alarms should not create timers
assert len(window._reminder_timers) == 0
def test_parse_today_alarms_no_text(qtbot, app, tmp_db_cfg, fresh_db):
"""Test alarm with no text before emoji uses fallback."""
from PySide6.QtCore import QTime
s = get_settings()
s.setValue("db/default_db", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key)
s.setValue("ui/idle_minutes", 0)
s.setValue("ui/theme", "light")
s.setValue("ui/move_todos", True)
s.setValue("ui/tags", True)
s.setValue("ui/time_log", True)
s.setValue("ui/reminders", True)
s.setValue("ui/locale", "en")
s.setValue("ui/font_size", 11)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
window = MainWindow(themes=themes)
qtbot.addWidget(window)
window.show()
# Open today's date
today_qdate = QDate.currentDate()
window._open_date_in_tab(today_qdate)
# Set content with alarm but no text
future_time = QTime.currentTime().addSecs(3600)
alarm_text = f"{future_time.hour():02d}:{future_time.minute():02d}"
window.editor.current_date = today_qdate
window.editor.setPlainText(alarm_text)
# Clear any existing timers
window._reminder_timers = []
# Trigger alarm parsing
window._rebuild_reminders_for_today()
# Timer should be created even without text
assert len(window._reminder_timers) > 0
def test_open_history_with_editor(qtbot, app, tmp_db_cfg, fresh_db):
"""Test opening history when editor has content."""
from unittest.mock import patch
s = get_settings()
s.setValue("db/default_db", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key)
s.setValue("ui/idle_minutes", 0)
s.setValue("ui/theme", "light")
s.setValue("ui/move_todos", True)
s.setValue("ui/tags", True)
s.setValue("ui/time_log", True)
s.setValue("ui/reminders", True)
s.setValue("ui/locale", "en")
s.setValue("ui/font_size", 11)
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
window = MainWindow(themes=themes)
qtbot.addWidget(window)
window.show()
# Create some history
today = date.today().isoformat()
fresh_db.save_new_version(today, "v1", "note1")
fresh_db.save_new_version(today, "v2", "note2")
# Open today's date
window._open_date_in_tab(QDate.fromString(today, "yyyy-MM-dd"))
# Open history
with patch("bouquin.history_dialog.HistoryDialog.exec") as mock_exec:
mock_exec.return_value = False # User cancels
window._open_history()
# HistoryDialog should have been created and shown
mock_exec.assert_called_once()

View file

@ -1,7 +1,7 @@
import base64
import pytest
from PySide6.QtCore import Qt, QPoint
from PySide6.QtCore import Qt, QPoint, QMimeData, QUrl
from PySide6.QtGui import (
QImage,
QColor,
@ -2216,3 +2216,243 @@ def test_markdown_highlighter_theme_change(qtbot, app):
# Highlighter should update
# We can't directly test the visual change, but verify it doesn't crash
assert highlighter is not None
def test_auto_pair_skip_closing_bracket(editor, qtbot):
"""Test skipping over closing brackets when auto-pairing."""
# Insert opening bracket
editor.insertPlainText("(")
# Type closing bracket - should skip over the auto-inserted one
event = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_ParenRight, Qt.NoModifier, ")")
editor.keyPressEvent(event)
# Should have only one pair of brackets
text = editor.toPlainText()
assert text.count("(") == 1
assert text.count(")") == 1
def test_apply_heading(editor, qtbot):
"""Test applying heading to text."""
# Insert some text
editor.insertPlainText("Heading Text")
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.StartOfLine)
editor.setTextCursor(cursor)
# Apply heading - size >= 24 creates level 1 heading
editor.apply_heading(24)
text = editor.toPlainText()
assert text.startswith("#")
def test_handle_return_in_code_block(editor, qtbot):
"""Test pressing return inside a code block."""
# Create a code block
editor.insertPlainText("```python\nprint('hello')")
# Place cursor at end
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.End)
editor.setTextCursor(cursor)
# Press return - should maintain indentation
event = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Return, Qt.NoModifier, "\n")
editor.keyPressEvent(event)
# Should have added a new line
text = editor.toPlainText()
assert text.count("\n") >= 2
def test_handle_return_in_list_empty_item(editor, qtbot):
"""Test pressing return in an empty list item."""
# Create list with empty item
editor.insertPlainText("- item\n- ")
# Place cursor at end of empty item
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.End)
editor.setTextCursor(cursor)
# Press return - should end the list
event = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Return, Qt.NoModifier, "\n")
editor.keyPressEvent(event)
text = editor.toPlainText()
# Should have processed the empty list marker
lines = text.split("\n")
assert len(lines) >= 2
def test_handle_backspace_in_empty_list_item(editor, qtbot):
"""Test pressing backspace in an empty list item."""
# Create list with cursor after marker
editor.insertPlainText("- ")
# Place cursor at end
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.End)
editor.setTextCursor(cursor)
# Press backspace - should remove list marker
event = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Backspace, Qt.NoModifier, "")
editor.keyPressEvent(event)
text = editor.toPlainText()
# List marker handling
assert len(text) <= 2
def test_tab_key_handling(editor, qtbot):
"""Test tab key handling in editor."""
# Create a list item
editor.insertPlainText("- item")
# Place cursor in the item
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.End)
editor.setTextCursor(cursor)
# Press tab
event = QKeyEvent(QKeyEvent.KeyPress, Qt.Key_Tab, Qt.NoModifier, "\t")
editor.keyPressEvent(event)
# Should have processed the tab
text = editor.toPlainText()
assert len(text) >= 6 # At least "- item" plus tab
def test_drag_enter_with_urls(editor, qtbot):
"""Test drag and drop with URLs."""
from PySide6.QtGui import QDragEnterEvent
# Create mime data with URLs
mime_data = QMimeData()
mime_data.setUrls([QUrl("file:///tmp/test.txt")])
# Create drag enter event
event = QDragEnterEvent(
editor.rect().center(), Qt.CopyAction, mime_data, Qt.LeftButton, Qt.NoModifier
)
# Handle drag enter
editor.dragEnterEvent(event)
# Should accept the event
assert event.isAccepted()
def test_drag_enter_with_text(editor, qtbot):
"""Test drag and drop with plain text."""
from PySide6.QtGui import QDragEnterEvent
# Create mime data with text
mime_data = QMimeData()
mime_data.setText("dragged text")
# Create drag enter event
event = QDragEnterEvent(
editor.rect().center(), Qt.CopyAction, mime_data, Qt.LeftButton, Qt.NoModifier
)
# Handle drag enter
editor.dragEnterEvent(event)
# Should accept text drag
assert event.isAccepted()
def test_highlighter_dark_mode_code_blocks(app, qtbot, tmp_path):
"""Test code block highlighting in dark mode."""
# Get theme manager and set dark mode
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.DARK))
# Create editor with dark theme
editor = MarkdownEditor(theme_manager)
qtbot.addWidget(editor)
# Insert code block
editor.setPlainText("```python\nprint('hello')\n```")
# Force rehighlight
editor.highlighter.rehighlight()
# Verify no crash - actual color verification is difficult in tests
def test_highlighter_code_block_with_language(editor, qtbot):
"""Test syntax highlighting inside fenced code blocks with language."""
# Insert code block with language
editor.setPlainText('```python\ndef hello():\n print("world")\n```')
# Force rehighlight
editor.highlighter.rehighlight()
# Verify syntax highlighting was applied (lines 186-193)
# We can't easily verify the exact formatting, but we ensure no crash
def test_highlighter_bold_italic_overlap_detection(editor, qtbot):
"""Test that bold/italic formatting detects overlaps correctly."""
# Insert text with overlapping bold and triple-asterisk
editor.setPlainText("***bold and italic***")
# Force rehighlight
editor.highlighter.rehighlight()
# The overlap detection (lines 252, 264) should prevent issues
def test_highlighter_italic_edge_cases(editor, qtbot):
"""Test italic formatting edge cases."""
# Test edge case: avoiding stealing markers that are part of double
# This tests lines 267-270
editor.setPlainText("**not italic* text**")
# Force rehighlight
editor.highlighter.rehighlight()
# Test another edge case
editor.setPlainText("*italic but next to double**")
editor.highlighter.rehighlight()
def test_highlighter_multiple_markdown_elements(editor, qtbot):
"""Test highlighting document with multiple markdown elements."""
# Complex document with various elements
text = """# Heading 1
## Heading 2
**bold text** and *italic text*
```python
def test():
return True
```
- list item
- [ ] task item
[link](http://example.com)
"""
editor.setPlainText(text)
editor.highlighter.rehighlight()
# Verify no crashes with complex formatting
def test_highlighter_inline_code_vs_fence(editor, qtbot):
"""Test that inline code and fenced blocks are distinguished."""
text = """Inline `code` here
```
fenced block
```
"""
editor.setPlainText(text)
editor.highlighter.rehighlight()

View file

@ -7,7 +7,9 @@ from bouquin.reminders import (
ManageRemindersDialog,
)
from PySide6.QtCore import QDate, QTime
from PySide6.QtWidgets import QDialog, QMessageBox
from PySide6.QtWidgets import QDialog, QMessageBox, QWidget
from datetime import date, timedelta
def test_reminder_type_enum(app):
@ -655,3 +657,174 @@ def test_reminder_with_inactive_status(qtbot, app, fresh_db):
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_single_item(qtbot, fresh_db):
"""Test context menu for a single reminder item."""
reminder = Reminder(
id=None,
text="Test reminder",
reminder_type=ReminderType.ONCE,
time_str="14:30",
date_iso=date.today().isoformat(),
active=True,
)
fresh_db.save_reminder(reminder)
reminders_widget = UpcomingRemindersWidget(fresh_db)
qtbot.addWidget(reminders_widget)
reminders_widget.show()
# Refresh to populate the list
reminders_widget.refresh()
# Select the first item
if reminders_widget.reminder_list.count() > 0:
reminders_widget.reminder_list.setCurrentRow(0)
# Show context menu (won't actually display in tests)
reminders_widget._show_reminder_context_menu(
reminders_widget.reminder_list.pos()
)
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"

View file

@ -3,10 +3,9 @@ from datetime import datetime, timedelta, date
from bouquin import strings
from PySide6.QtCore import Qt, QPoint
from PySide6.QtWidgets import QLabel
from PySide6.QtCore import Qt, QPoint, QDate
from PySide6.QtWidgets import QLabel, QWidget
from PySide6.QtTest import QTest
from PySide6.QtCore import QDate
from bouquin.statistics_dialog import DateHeatmap, StatisticsDialog
@ -515,3 +514,123 @@ def test_statistics_dialog_metric_changes(qtbot, tmp_db_cfg, fresh_db):
for i in range(dialog.metric_combo.count()):
dialog.metric_combo.setCurrentIndex(i)
qtbot.wait(50)
def test_heatmap_date_beyond_end(qtbot, fresh_db):
"""Test clicking on a date beyond the end date in heatmap."""
# Create entries spanning a range
today = date.today()
start = today - timedelta(days=30)
data = {}
for i in range(20):
d = start + timedelta(days=i)
fresh_db.save_new_version(d.isoformat(), f"Entry {i}", f"note {i}")
data[d] = 1
w = QWidget()
qtbot.addWidget(w)
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
# Set data
heatmap.set_data(data)
# Try to click beyond the end date - should return early
# Calculate a position that would be beyond the end
if heatmap._start and heatmap._end:
cell_span = heatmap._cell + heatmap._gap
weeks = ((heatmap._end - heatmap._start).days + 6) // 7
# Click beyond the last week
x = heatmap._margin_left + (weeks + 1) * cell_span + 5
y = heatmap._margin_top + 3 * cell_span + 5
QTest.mouseClick(heatmap, Qt.LeftButton, Qt.NoModifier, QPoint(int(x), int(y)))
def test_heatmap_click_outside_grid(qtbot, fresh_db):
"""Test clicking outside the heatmap grid area."""
today = date.today()
start = today - timedelta(days=7)
data = {}
for i in range(7):
d = start + timedelta(days=i)
fresh_db.save_new_version(d.isoformat(), f"Entry {i}", f"note {i}")
data[d] = 1
w = QWidget()
qtbot.addWidget(w)
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
heatmap.set_data(data)
# Click in the margin (outside grid)
x = heatmap._margin_left - 10 # Before the grid
y = heatmap._margin_top - 10 # Above the grid
QTest.mouseClick(heatmap, Qt.LeftButton, Qt.NoModifier, QPoint(int(x), int(y)))
# Should not crash, just return early
def test_heatmap_click_invalid_row(qtbot, fresh_db):
"""Test clicking on an invalid row (>= 7)."""
today = date.today()
start = today - timedelta(days=7)
data = {}
for i in range(7):
d = start + timedelta(days=i)
fresh_db.save_new_version(d.isoformat(), f"Entry {i}", f"note {i}")
data[d] = 1
w = QWidget()
qtbot.addWidget(w)
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
heatmap.set_data(data)
# Click below row 6 (day of week > Sunday)
cell_span = heatmap._cell + heatmap._gap
x = heatmap._margin_left + 5
y = heatmap._margin_top + 7 * cell_span + 5 # Row 7, which is invalid
QTest.mouseClick(heatmap, Qt.LeftButton, Qt.NoModifier, QPoint(int(x), int(y)))
# Should return early, not crash
def test_heatmap_month_label_continuation(qtbot, fresh_db):
"""Test that month labels don't repeat when continuing in same month."""
# Create a date range that spans multiple weeks within the same month
today = date.today()
# Use a date that's guaranteed to be mid-month
start = date(today.year, today.month, 1)
data = {}
for i in range(21):
d = start + timedelta(days=i)
fresh_db.save_new_version(d.isoformat(), f"Entry {i}", f"note {i}")
data[d] = 1
w = QWidget()
qtbot.addWidget(w)
heatmap = DateHeatmap()
qtbot.addWidget(heatmap)
heatmap.show()
heatmap.set_data(data)
# Force a repaint to execute paintEvent
heatmap.repaint()
# The month continuation logic (line 175) should prevent duplicate labels
# We can't easily test the visual output, but we ensure no crash

View file

@ -477,15 +477,6 @@ def test_time_report_empty(fresh_db):
# ============================================================================
def test_time_log_widget_creation(qtbot, fresh_db):
"""TimeLogWidget can be created."""
widget = TimeLogWidget(fresh_db)
qtbot.addWidget(widget)
assert widget is not None
assert not widget.toggle_btn.isChecked()
assert not widget.body.isVisible()
def test_time_log_widget_toggle(qtbot, fresh_db):
"""Toggle expands/collapses the widget."""
widget = TimeLogWidget(fresh_db)
@ -2556,3 +2547,52 @@ def test_time_report_dialog_very_large_hours(qtbot, fresh_db):
# Check total label
assert "166" in dialog.total_label.text() or "167" in dialog.total_label.text()
def test_time_log_widget_creation(qtbot, fresh_db):
"""TimeLogWidget can be created."""
widget = TimeLogWidget(fresh_db)
qtbot.addWidget(widget)
assert widget is not None
assert not widget.toggle_btn.isChecked()
assert not widget.body.isVisible()
def test_time_log_set_current_date(qtbot, fresh_db):
"""Test setting the current date on the time log widget."""
widget = TimeLogWidget(fresh_db)
qtbot.addWidget(widget)
today = date.today().isoformat()
widget.set_current_date(today)
# Verify the current date was set
assert widget._current_date == today
def test_time_log_with_entry(qtbot, fresh_db):
"""Test time log widget with a time entry."""
# Add a project
proj_id = fresh_db.add_project("Test Project")
# Add activity
act_id = fresh_db.add_activity("Test Activity")
# Add a time log entry
today = date.today().isoformat()
fresh_db.add_time_log(
date_iso=today,
project_id=proj_id,
activity_id=act_id,
minutes=150,
note="Test note",
)
widget = TimeLogWidget(fresh_db)
qtbot.addWidget(widget)
widget.show()
# Set the date to today
widget.set_current_date(today)
# Widget should have been created successfully
assert widget is not None

View file

@ -510,3 +510,25 @@ def test_download_file_invalid_content_length(qtbot, app, tmp_path):
checker._download_file("http://example.com/file", dest_path)
assert dest_path.exists()
def test_version_checker_creation(qtbot):
"""Test creating a VersionChecker instance."""
widget = QWidget()
qtbot.addWidget(widget)
checker = VersionChecker(widget)
assert checker is not None
def test_current_version(qtbot):
"""Test getting the current version."""
widget = QWidget()
qtbot.addWidget(widget)
checker = VersionChecker(widget)
version = checker.current_version()
# Version should be a string
assert isinstance(version, str)
assert len(version) > 0