Many changes and new features:
* 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:
parent
26737fbfb2
commit
e0169db52a
28 changed files with 4191 additions and 17 deletions
|
|
@ -36,7 +36,16 @@ def tmp_db_cfg(tmp_path):
|
|||
default_db = tmp_path / "notebook.db"
|
||||
key = "test-secret-key"
|
||||
return DBConfig(
|
||||
path=default_db, key=key, idle_minutes=0, theme="light", move_todos=True
|
||||
path=default_db,
|
||||
key=key,
|
||||
idle_minutes=0,
|
||||
theme="light",
|
||||
move_todos=True,
|
||||
tags=True,
|
||||
time_log=True,
|
||||
reminders=True,
|
||||
locale="en",
|
||||
font_size=11,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
398
tests/test_code_highlighter.py
Normal file
398
tests/test_code_highlighter.py
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
from bouquin.code_highlighter import CodeHighlighter, CodeBlockMetadata
|
||||
from PySide6.QtGui import QTextCharFormat, QFont
|
||||
|
||||
|
||||
def test_get_language_patterns_python(app):
|
||||
"""Test getting highlighting patterns for Python."""
|
||||
patterns = CodeHighlighter.get_language_patterns("python")
|
||||
|
||||
assert len(patterns) > 0
|
||||
# Should have comment pattern
|
||||
assert any("#" in p[0] for p in patterns)
|
||||
# Should have string patterns
|
||||
assert any('"' in p[0] for p in patterns)
|
||||
# Should have keyword patterns
|
||||
assert any("keyword" == p[1] for p in patterns)
|
||||
|
||||
|
||||
def test_get_language_patterns_javascript(app):
|
||||
"""Test getting highlighting patterns for JavaScript."""
|
||||
patterns = CodeHighlighter.get_language_patterns("javascript")
|
||||
|
||||
assert len(patterns) > 0
|
||||
# Should have // comment pattern
|
||||
assert any("//" in p[0] for p in patterns)
|
||||
# Should have /* */ comment pattern (with escaped asterisks in regex)
|
||||
assert any(r"/\*" in p[0] for p in patterns)
|
||||
|
||||
|
||||
def test_get_language_patterns_php(app):
|
||||
"""Test getting highlighting patterns for PHP."""
|
||||
patterns = CodeHighlighter.get_language_patterns("php")
|
||||
|
||||
assert len(patterns) > 0
|
||||
# Should have # comment pattern
|
||||
assert any("#" in p[0] for p in patterns)
|
||||
# Should have // comment pattern
|
||||
assert any("//" in p[0] for p in patterns)
|
||||
# Should have /* */ comment pattern (with escaped asterisks in regex)
|
||||
assert any(r"/\*" in p[0] for p in patterns)
|
||||
|
||||
|
||||
def test_get_language_patterns_bash(app):
|
||||
"""Test getting highlighting patterns for Bash."""
|
||||
patterns = CodeHighlighter.get_language_patterns("bash")
|
||||
|
||||
assert len(patterns) > 0
|
||||
# Should have # comment pattern
|
||||
assert any("#" in p[0] for p in patterns)
|
||||
# Should have bash keywords
|
||||
keyword_patterns = [p for p in patterns if p[1] == "keyword"]
|
||||
assert len(keyword_patterns) > 0
|
||||
|
||||
|
||||
def test_get_language_patterns_html(app):
|
||||
"""Test getting highlighting patterns for HTML."""
|
||||
patterns = CodeHighlighter.get_language_patterns("html")
|
||||
|
||||
assert len(patterns) > 0
|
||||
# Should have tag pattern
|
||||
assert any("tag" == p[1] for p in patterns)
|
||||
# Should have HTML comment pattern
|
||||
assert any("<!--" in p[0] for p in patterns)
|
||||
|
||||
|
||||
def test_get_language_patterns_css(app):
|
||||
"""Test getting highlighting patterns for CSS."""
|
||||
patterns = CodeHighlighter.get_language_patterns("css")
|
||||
|
||||
assert len(patterns) > 0
|
||||
# Should have // comment pattern
|
||||
assert any("//" in p[0] for p in patterns)
|
||||
# Should have CSS properties as keywords
|
||||
keyword_patterns = [p for p in patterns if p[1] == "keyword"]
|
||||
assert len(keyword_patterns) > 0
|
||||
|
||||
|
||||
def test_get_language_patterns_unknown_language(app):
|
||||
"""Test getting patterns for an unknown language."""
|
||||
patterns = CodeHighlighter.get_language_patterns("unknown-lang")
|
||||
|
||||
# Should still return basic patterns (strings, numbers)
|
||||
assert len(patterns) > 0
|
||||
assert any("string" == p[1] for p in patterns)
|
||||
assert any("number" == p[1] for p in patterns)
|
||||
|
||||
|
||||
def test_get_language_patterns_case_insensitive(app):
|
||||
"""Test that language matching is case insensitive."""
|
||||
patterns_lower = CodeHighlighter.get_language_patterns("python")
|
||||
patterns_upper = CodeHighlighter.get_language_patterns("PYTHON")
|
||||
patterns_mixed = CodeHighlighter.get_language_patterns("PyThOn")
|
||||
|
||||
assert len(patterns_lower) == len(patterns_upper)
|
||||
assert len(patterns_lower) == len(patterns_mixed)
|
||||
|
||||
|
||||
def test_get_format_for_type_keyword(app):
|
||||
"""Test getting format for keyword type."""
|
||||
base_format = QTextCharFormat()
|
||||
fmt = CodeHighlighter.get_format_for_type("keyword", base_format)
|
||||
|
||||
assert fmt.fontWeight() == QFont.Weight.Bold
|
||||
assert fmt.foreground().color().blue() > 0 # Should have blue-ish color
|
||||
|
||||
|
||||
def test_get_format_for_type_string(app):
|
||||
"""Test getting format for string type."""
|
||||
base_format = QTextCharFormat()
|
||||
fmt = CodeHighlighter.get_format_for_type("string", base_format)
|
||||
|
||||
# Should have orangish color
|
||||
color = fmt.foreground().color()
|
||||
assert color.red() > 100
|
||||
|
||||
|
||||
def test_get_format_for_type_comment(app):
|
||||
"""Test getting format for comment type."""
|
||||
base_format = QTextCharFormat()
|
||||
fmt = CodeHighlighter.get_format_for_type("comment", base_format)
|
||||
|
||||
assert fmt.fontItalic() is True
|
||||
# Should have greenish color
|
||||
color = fmt.foreground().color()
|
||||
assert color.green() > 0
|
||||
|
||||
|
||||
def test_get_format_for_type_number(app):
|
||||
"""Test getting format for number type."""
|
||||
base_format = QTextCharFormat()
|
||||
fmt = CodeHighlighter.get_format_for_type("number", base_format)
|
||||
|
||||
# Should have some color
|
||||
color = fmt.foreground().color()
|
||||
assert color.isValid()
|
||||
|
||||
|
||||
def test_get_format_for_type_tag(app):
|
||||
"""Test getting format for HTML tag type."""
|
||||
base_format = QTextCharFormat()
|
||||
fmt = CodeHighlighter.get_format_for_type("tag", base_format)
|
||||
|
||||
# Should have cyan-ish color
|
||||
color = fmt.foreground().color()
|
||||
assert color.green() > 0
|
||||
assert color.blue() > 0
|
||||
|
||||
|
||||
def test_get_format_for_type_unknown(app):
|
||||
"""Test getting format for unknown type."""
|
||||
base_format = QTextCharFormat()
|
||||
fmt = CodeHighlighter.get_format_for_type("unknown", base_format)
|
||||
|
||||
# Should return a valid format (based on base_format)
|
||||
assert fmt is not None
|
||||
|
||||
|
||||
def test_code_block_metadata_init(app):
|
||||
"""Test CodeBlockMetadata initialization."""
|
||||
metadata = CodeBlockMetadata()
|
||||
|
||||
assert len(metadata._block_languages) == 0
|
||||
|
||||
|
||||
def test_code_block_metadata_set_get_language(app):
|
||||
"""Test setting and getting language for a block."""
|
||||
metadata = CodeBlockMetadata()
|
||||
|
||||
metadata.set_language(0, "python")
|
||||
metadata.set_language(5, "javascript")
|
||||
|
||||
assert metadata.get_language(0) == "python"
|
||||
assert metadata.get_language(5) == "javascript"
|
||||
assert metadata.get_language(10) is None
|
||||
|
||||
|
||||
def test_code_block_metadata_set_language_case_normalization(app):
|
||||
"""Test that language is normalized to lowercase."""
|
||||
metadata = CodeBlockMetadata()
|
||||
|
||||
metadata.set_language(0, "PYTHON")
|
||||
metadata.set_language(1, "JavaScript")
|
||||
|
||||
assert metadata.get_language(0) == "python"
|
||||
assert metadata.get_language(1) == "javascript"
|
||||
|
||||
|
||||
def test_code_block_metadata_serialize_empty(app):
|
||||
"""Test serializing empty metadata."""
|
||||
metadata = CodeBlockMetadata()
|
||||
|
||||
result = metadata.serialize()
|
||||
assert result == ""
|
||||
|
||||
|
||||
def test_code_block_metadata_serialize(app):
|
||||
"""Test serializing metadata."""
|
||||
metadata = CodeBlockMetadata()
|
||||
metadata.set_language(0, "python")
|
||||
metadata.set_language(3, "javascript")
|
||||
|
||||
result = metadata.serialize()
|
||||
|
||||
assert "<!-- code-langs:" in result
|
||||
assert "0:python" in result
|
||||
assert "3:javascript" in result
|
||||
assert "-->" in result
|
||||
|
||||
|
||||
def test_code_block_metadata_serialize_sorted(app):
|
||||
"""Test that serialized metadata is sorted by block number."""
|
||||
metadata = CodeBlockMetadata()
|
||||
metadata.set_language(5, "python")
|
||||
metadata.set_language(2, "javascript")
|
||||
metadata.set_language(8, "bash")
|
||||
|
||||
result = metadata.serialize()
|
||||
|
||||
# Find positions in string
|
||||
pos_2 = result.find("2:")
|
||||
pos_5 = result.find("5:")
|
||||
pos_8 = result.find("8:")
|
||||
|
||||
# Should be in order
|
||||
assert pos_2 < pos_5 < pos_8
|
||||
|
||||
|
||||
def test_code_block_metadata_deserialize(app):
|
||||
"""Test deserializing metadata."""
|
||||
metadata = CodeBlockMetadata()
|
||||
text = (
|
||||
"Some content\n<!-- code-langs: 0:python,3:javascript,5:bash -->\nMore content"
|
||||
)
|
||||
|
||||
metadata.deserialize(text)
|
||||
|
||||
assert metadata.get_language(0) == "python"
|
||||
assert metadata.get_language(3) == "javascript"
|
||||
assert metadata.get_language(5) == "bash"
|
||||
|
||||
|
||||
def test_code_block_metadata_deserialize_empty(app):
|
||||
"""Test deserializing from text without metadata."""
|
||||
metadata = CodeBlockMetadata()
|
||||
metadata.set_language(0, "python") # Set some initial data
|
||||
|
||||
text = "Just some regular text with no metadata"
|
||||
metadata.deserialize(text)
|
||||
|
||||
# Should clear existing data
|
||||
assert len(metadata._block_languages) == 0
|
||||
|
||||
|
||||
def test_code_block_metadata_deserialize_invalid_format(app):
|
||||
"""Test deserializing with invalid format."""
|
||||
metadata = CodeBlockMetadata()
|
||||
text = "<!-- code-langs: invalid,format,here -->"
|
||||
|
||||
metadata.deserialize(text)
|
||||
|
||||
# Should handle gracefully, resulting in empty or minimal data
|
||||
# Pairs without ':' should be skipped
|
||||
assert len(metadata._block_languages) == 0
|
||||
|
||||
|
||||
def test_code_block_metadata_deserialize_invalid_block_number(app):
|
||||
"""Test deserializing with invalid block number."""
|
||||
metadata = CodeBlockMetadata()
|
||||
text = "<!-- code-langs: abc:python,3:javascript -->"
|
||||
|
||||
metadata.deserialize(text)
|
||||
|
||||
# Should skip invalid block number 'abc'
|
||||
assert metadata.get_language(3) == "javascript"
|
||||
assert "abc" not in str(metadata._block_languages)
|
||||
|
||||
|
||||
def test_code_block_metadata_round_trip(app):
|
||||
"""Test serializing and deserializing preserves data."""
|
||||
metadata1 = CodeBlockMetadata()
|
||||
metadata1.set_language(0, "python")
|
||||
metadata1.set_language(2, "javascript")
|
||||
metadata1.set_language(7, "bash")
|
||||
|
||||
serialized = metadata1.serialize()
|
||||
|
||||
metadata2 = CodeBlockMetadata()
|
||||
metadata2.deserialize(serialized)
|
||||
|
||||
assert metadata2.get_language(0) == "python"
|
||||
assert metadata2.get_language(2) == "javascript"
|
||||
assert metadata2.get_language(7) == "bash"
|
||||
|
||||
|
||||
def test_python_keywords_present(app):
|
||||
"""Test that Python keywords are defined."""
|
||||
keywords = CodeHighlighter.KEYWORDS.get("python", [])
|
||||
|
||||
assert "def" in keywords
|
||||
assert "class" in keywords
|
||||
assert "if" in keywords
|
||||
assert "for" in keywords
|
||||
assert "import" in keywords
|
||||
|
||||
|
||||
def test_javascript_keywords_present(app):
|
||||
"""Test that JavaScript keywords are defined."""
|
||||
keywords = CodeHighlighter.KEYWORDS.get("javascript", [])
|
||||
|
||||
assert "function" in keywords
|
||||
assert "const" in keywords
|
||||
assert "let" in keywords
|
||||
assert "var" in keywords
|
||||
assert "class" in keywords
|
||||
|
||||
|
||||
def test_php_keywords_present(app):
|
||||
"""Test that PHP keywords are defined."""
|
||||
keywords = CodeHighlighter.KEYWORDS.get("php", [])
|
||||
|
||||
assert "function" in keywords
|
||||
assert "class" in keywords
|
||||
assert "echo" in keywords
|
||||
assert "require" in keywords
|
||||
|
||||
|
||||
def test_bash_keywords_present(app):
|
||||
"""Test that Bash keywords are defined."""
|
||||
keywords = CodeHighlighter.KEYWORDS.get("bash", [])
|
||||
|
||||
assert "if" in keywords
|
||||
assert "then" in keywords
|
||||
assert "fi" in keywords
|
||||
assert "for" in keywords
|
||||
|
||||
|
||||
def test_html_keywords_present(app):
|
||||
"""Test that HTML keywords are defined."""
|
||||
keywords = CodeHighlighter.KEYWORDS.get("html", [])
|
||||
|
||||
assert "div" in keywords
|
||||
assert "span" in keywords
|
||||
assert "body" in keywords
|
||||
assert "html" in keywords
|
||||
|
||||
|
||||
def test_css_keywords_present(app):
|
||||
"""Test that CSS keywords are defined."""
|
||||
keywords = CodeHighlighter.KEYWORDS.get("css", [])
|
||||
|
||||
assert "color" in keywords
|
||||
assert "background" in keywords
|
||||
assert "margin" in keywords
|
||||
assert "padding" in keywords
|
||||
|
||||
|
||||
def test_all_patterns_have_string_and_number(app):
|
||||
"""Test that all languages have string and number patterns."""
|
||||
languages = ["python", "javascript", "php", "bash", "html", "css"]
|
||||
|
||||
for lang in languages:
|
||||
patterns = CodeHighlighter.get_language_patterns(lang)
|
||||
pattern_types = [p[1] for p in patterns]
|
||||
|
||||
assert "string" in pattern_types, f"{lang} should have string pattern"
|
||||
assert "number" in pattern_types, f"{lang} should have number pattern"
|
||||
|
||||
|
||||
def test_patterns_have_regex_format(app):
|
||||
"""Test that patterns are in regex format."""
|
||||
patterns = CodeHighlighter.get_language_patterns("python")
|
||||
|
||||
for pattern, pattern_type in patterns:
|
||||
# Each pattern should be a string (regex pattern)
|
||||
assert isinstance(pattern, str)
|
||||
# Each type should be a string
|
||||
assert isinstance(pattern_type, str)
|
||||
|
||||
|
||||
def test_code_block_metadata_update_language(app):
|
||||
"""Test updating language for existing block."""
|
||||
metadata = CodeBlockMetadata()
|
||||
|
||||
metadata.set_language(0, "python")
|
||||
assert metadata.get_language(0) == "python"
|
||||
|
||||
metadata.set_language(0, "javascript")
|
||||
assert metadata.get_language(0) == "javascript"
|
||||
|
||||
|
||||
def test_get_format_preserves_base_format_properties(app):
|
||||
"""Test that get_format_for_type preserves base format properties."""
|
||||
base_format = QTextCharFormat()
|
||||
base_format.setFontPointSize(14)
|
||||
|
||||
fmt = CodeHighlighter.get_format_for_type("keyword", base_format)
|
||||
|
||||
# Should be based on the base_format
|
||||
assert isinstance(fmt, QTextCharFormat)
|
||||
|
|
@ -25,6 +25,11 @@ def test_main_window_loads_and_saves(qtbot, app, tmp_db_cfg, fresh_db):
|
|||
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))
|
||||
w = MainWindow(themes=themes)
|
||||
|
|
|
|||
354
tests/test_pomodoro_timer.py
Normal file
354
tests/test_pomodoro_timer.py
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
from unittest.mock import Mock, patch
|
||||
from bouquin.pomodoro_timer import PomodoroTimer, PomodoroManager
|
||||
|
||||
|
||||
def test_pomodoro_timer_init(qtbot, app, fresh_db):
|
||||
"""Test PomodoroTimer initialization."""
|
||||
task_text = "Write unit tests"
|
||||
timer = PomodoroTimer(task_text)
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
assert timer._task_text == task_text
|
||||
assert timer._elapsed_seconds == 0
|
||||
assert timer._running is False
|
||||
assert timer.time_label.text() == "00:00:00"
|
||||
assert timer.stop_btn.isEnabled() is False
|
||||
|
||||
|
||||
def test_pomodoro_timer_start(qtbot, app):
|
||||
"""Test starting the timer."""
|
||||
timer = PomodoroTimer("Test task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
timer._toggle_timer()
|
||||
|
||||
assert timer._running is True
|
||||
assert timer.stop_btn.isEnabled() is True
|
||||
|
||||
|
||||
def test_pomodoro_timer_pause(qtbot, app):
|
||||
"""Test pausing the timer."""
|
||||
timer = PomodoroTimer("Test task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
# Start the timer
|
||||
timer._toggle_timer()
|
||||
assert timer._running is True
|
||||
|
||||
# Pause the timer
|
||||
timer._toggle_timer()
|
||||
assert timer._running is False
|
||||
|
||||
|
||||
def test_pomodoro_timer_resume(qtbot, app):
|
||||
"""Test resuming the timer after pause."""
|
||||
timer = PomodoroTimer("Test task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
# Start, pause, then resume
|
||||
timer._toggle_timer() # Start
|
||||
timer._toggle_timer() # Pause
|
||||
timer._toggle_timer() # Resume
|
||||
|
||||
assert timer._running is True
|
||||
|
||||
|
||||
def test_pomodoro_timer_tick(qtbot, app):
|
||||
"""Test timer tick increments elapsed time."""
|
||||
timer = PomodoroTimer("Test task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
initial_time = timer._elapsed_seconds
|
||||
timer._tick()
|
||||
|
||||
assert timer._elapsed_seconds == initial_time + 1
|
||||
|
||||
|
||||
def test_pomodoro_timer_display_update(qtbot, app):
|
||||
"""Test display updates with various elapsed times."""
|
||||
timer = PomodoroTimer("Test task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
# Test 0 seconds
|
||||
timer._elapsed_seconds = 0
|
||||
timer._update_display()
|
||||
assert timer.time_label.text() == "00:00:00"
|
||||
|
||||
# Test 65 seconds (1 min 5 sec)
|
||||
timer._elapsed_seconds = 65
|
||||
timer._update_display()
|
||||
assert timer.time_label.text() == "00:01:05"
|
||||
|
||||
# Test 3665 seconds (1 hour 1 min 5 sec)
|
||||
timer._elapsed_seconds = 3665
|
||||
timer._update_display()
|
||||
assert timer.time_label.text() == "01:01:05"
|
||||
|
||||
# Test 3600 seconds (1 hour exactly)
|
||||
timer._elapsed_seconds = 3600
|
||||
timer._update_display()
|
||||
assert timer.time_label.text() == "01:00:00"
|
||||
|
||||
|
||||
def test_pomodoro_timer_stop_and_log_running(qtbot, app):
|
||||
"""Test stopping the timer while it's running."""
|
||||
timer = PomodoroTimer("Test task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
# Start the timer
|
||||
timer._toggle_timer()
|
||||
timer._elapsed_seconds = 100
|
||||
|
||||
# Connect a mock to the signal
|
||||
signal_received = []
|
||||
timer.timerStopped.connect(lambda s, t: signal_received.append((s, t)))
|
||||
|
||||
timer._stop_and_log()
|
||||
|
||||
assert timer._running is False
|
||||
assert len(signal_received) == 1
|
||||
assert signal_received[0][0] == 100 # elapsed seconds
|
||||
assert signal_received[0][1] == "Test task"
|
||||
|
||||
|
||||
def test_pomodoro_timer_stop_and_log_paused(qtbot, app):
|
||||
"""Test stopping the timer when it's paused."""
|
||||
timer = PomodoroTimer("Test task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
timer._elapsed_seconds = 50
|
||||
|
||||
signal_received = []
|
||||
timer.timerStopped.connect(lambda s, t: signal_received.append((s, t)))
|
||||
|
||||
timer._stop_and_log()
|
||||
|
||||
assert len(signal_received) == 1
|
||||
assert signal_received[0][0] == 50
|
||||
|
||||
|
||||
def test_pomodoro_timer_multiple_ticks(qtbot, app):
|
||||
"""Test multiple timer ticks."""
|
||||
timer = PomodoroTimer("Test task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
for i in range(10):
|
||||
timer._tick()
|
||||
|
||||
assert timer._elapsed_seconds == 10
|
||||
assert "00:00:10" in timer.time_label.text()
|
||||
|
||||
|
||||
def test_pomodoro_timer_modal_state(qtbot, app):
|
||||
"""Test that timer is non-modal."""
|
||||
timer = PomodoroTimer("Test task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
assert timer.isModal() is False
|
||||
|
||||
|
||||
def test_pomodoro_timer_window_title(qtbot, app):
|
||||
"""Test timer window title."""
|
||||
timer = PomodoroTimer("Test task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
# Window title should contain some reference to timer/pomodoro
|
||||
assert len(timer.windowTitle()) > 0
|
||||
|
||||
|
||||
def test_pomodoro_manager_init(app, fresh_db):
|
||||
"""Test PomodoroManager initialization."""
|
||||
parent = Mock()
|
||||
manager = PomodoroManager(fresh_db, parent)
|
||||
|
||||
assert manager._db is fresh_db
|
||||
assert manager._parent is parent
|
||||
assert manager._active_timer is None
|
||||
|
||||
|
||||
def test_pomodoro_manager_start_timer(qtbot, app, fresh_db):
|
||||
"""Test starting a timer through the manager."""
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
manager = PomodoroManager(fresh_db, parent)
|
||||
|
||||
line_text = "Important task"
|
||||
date_iso = "2024-01-15"
|
||||
|
||||
manager.start_timer_for_line(line_text, date_iso)
|
||||
|
||||
assert manager._active_timer is not None
|
||||
assert manager._active_timer._task_text == line_text
|
||||
qtbot.addWidget(manager._active_timer)
|
||||
|
||||
|
||||
def test_pomodoro_manager_replace_active_timer(qtbot, app, fresh_db):
|
||||
"""Test that starting a new timer closes the previous one."""
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
manager = PomodoroManager(fresh_db, parent)
|
||||
|
||||
# Start first timer
|
||||
manager.start_timer_for_line("Task 1", "2024-01-15")
|
||||
first_timer = manager._active_timer
|
||||
qtbot.addWidget(first_timer)
|
||||
first_timer.show()
|
||||
|
||||
# Start second timer
|
||||
manager.start_timer_for_line("Task 2", "2024-01-16")
|
||||
second_timer = manager._active_timer
|
||||
qtbot.addWidget(second_timer)
|
||||
|
||||
assert first_timer is not second_timer
|
||||
assert second_timer._task_text == "Task 2"
|
||||
|
||||
|
||||
def test_pomodoro_manager_on_timer_stopped_minimum_hours(
|
||||
qtbot, app, fresh_db, monkeypatch
|
||||
):
|
||||
"""Test that timer stopped with very short time logs minimum hours."""
|
||||
parent = Mock()
|
||||
manager = PomodoroManager(fresh_db, parent)
|
||||
|
||||
# Mock TimeLogDialog to avoid actually showing it
|
||||
mock_dialog = Mock()
|
||||
mock_dialog.hours_spin = Mock()
|
||||
mock_dialog.note = Mock()
|
||||
mock_dialog.exec = Mock()
|
||||
|
||||
with patch("bouquin.pomodoro_timer.TimeLogDialog", return_value=mock_dialog):
|
||||
manager._on_timer_stopped(10, "Quick task", "2024-01-15")
|
||||
|
||||
# Should set minimum of 0.25 hours
|
||||
mock_dialog.hours_spin.setValue.assert_called_once()
|
||||
hours_set = mock_dialog.hours_spin.setValue.call_args[0][0]
|
||||
assert hours_set >= 0.25
|
||||
|
||||
|
||||
def test_pomodoro_manager_on_timer_stopped_rounding(qtbot, app, fresh_db, monkeypatch):
|
||||
"""Test that elapsed time is properly rounded to decimal hours."""
|
||||
parent = Mock()
|
||||
manager = PomodoroManager(fresh_db, parent)
|
||||
|
||||
mock_dialog = Mock()
|
||||
mock_dialog.hours_spin = Mock()
|
||||
mock_dialog.note = Mock()
|
||||
mock_dialog.exec = Mock()
|
||||
|
||||
with patch("bouquin.pomodoro_timer.TimeLogDialog", return_value=mock_dialog):
|
||||
# Test with 1800 seconds (30 minutes)
|
||||
manager._on_timer_stopped(1800, "Task", "2024-01-15")
|
||||
|
||||
mock_dialog.hours_spin.setValue.assert_called_once()
|
||||
hours_set = mock_dialog.hours_spin.setValue.call_args[0][0]
|
||||
# Should round up and be a multiple of 0.25
|
||||
assert hours_set > 0
|
||||
assert hours_set * 4 == int(hours_set * 4) # Multiple of 0.25
|
||||
|
||||
|
||||
def test_pomodoro_manager_on_timer_stopped_prefills_note(
|
||||
qtbot, app, fresh_db, monkeypatch
|
||||
):
|
||||
"""Test that timer stopped pre-fills the note in time log dialog."""
|
||||
parent = Mock()
|
||||
manager = PomodoroManager(fresh_db, parent)
|
||||
|
||||
mock_dialog = Mock()
|
||||
mock_dialog.hours_spin = Mock()
|
||||
mock_dialog.note = Mock()
|
||||
mock_dialog.exec = Mock()
|
||||
|
||||
task_text = "Write documentation"
|
||||
|
||||
with patch("bouquin.pomodoro_timer.TimeLogDialog", return_value=mock_dialog):
|
||||
manager._on_timer_stopped(3600, task_text, "2024-01-15")
|
||||
|
||||
mock_dialog.note.setText.assert_called_once_with(task_text)
|
||||
|
||||
|
||||
def test_pomodoro_manager_timer_stopped_signal_connection(
|
||||
qtbot, app, fresh_db, monkeypatch
|
||||
):
|
||||
"""Test that timer stopped signal is properly connected."""
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
manager = PomodoroManager(fresh_db, parent)
|
||||
|
||||
# Mock TimeLogDialog
|
||||
mock_dialog = Mock()
|
||||
mock_dialog.hours_spin = Mock()
|
||||
mock_dialog.note = Mock()
|
||||
mock_dialog.exec = Mock()
|
||||
|
||||
with patch("bouquin.pomodoro_timer.TimeLogDialog", return_value=mock_dialog):
|
||||
manager.start_timer_for_line("Task", "2024-01-15")
|
||||
timer = manager._active_timer
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
# Simulate timer stopped
|
||||
timer._elapsed_seconds = 1000
|
||||
timer._stop_and_log()
|
||||
|
||||
# TimeLogDialog should have been created
|
||||
assert mock_dialog.exec.called
|
||||
|
||||
|
||||
def test_pomodoro_timer_accepts_parent(qtbot, app):
|
||||
"""Test that timer accepts a parent widget."""
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
timer = PomodoroTimer("Task", parent)
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
assert timer.parent() is parent
|
||||
|
||||
|
||||
def test_pomodoro_manager_no_active_timer_initially(app, fresh_db):
|
||||
"""Test that manager starts with no active timer."""
|
||||
parent = Mock()
|
||||
manager = PomodoroManager(fresh_db, parent)
|
||||
|
||||
assert manager._active_timer is None
|
||||
|
||||
|
||||
def test_pomodoro_timer_start_stop_cycle(qtbot, app):
|
||||
"""Test a complete start-stop cycle."""
|
||||
timer = PomodoroTimer("Complete cycle")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
signal_received = []
|
||||
timer.timerStopped.connect(lambda s, t: signal_received.append((s, t)))
|
||||
|
||||
# Start
|
||||
timer._toggle_timer()
|
||||
assert timer._running is True
|
||||
|
||||
# Simulate some ticks
|
||||
for _ in range(5):
|
||||
timer._tick()
|
||||
|
||||
# Stop
|
||||
timer._stop_and_log()
|
||||
assert timer._running is False
|
||||
assert len(signal_received) == 1
|
||||
assert signal_received[0][0] == 5
|
||||
|
||||
|
||||
def test_pomodoro_timer_long_elapsed_time(qtbot, app):
|
||||
"""Test display with very long elapsed time."""
|
||||
timer = PomodoroTimer("Long task")
|
||||
qtbot.addWidget(timer)
|
||||
|
||||
# Set to 2 hours, 34 minutes, 56 seconds
|
||||
timer._elapsed_seconds = 2 * 3600 + 34 * 60 + 56
|
||||
timer._update_display()
|
||||
|
||||
assert timer.time_label.text() == "02:34:56"
|
||||
657
tests/test_reminders.py
Normal file
657
tests/test_reminders.py
Normal 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()
|
||||
|
|
@ -15,7 +15,11 @@ def _clear_db_settings():
|
|||
"ui/idle_minutes",
|
||||
"ui/theme",
|
||||
"ui/move_todos",
|
||||
"ui/tags",
|
||||
"ui/time_log",
|
||||
"ui/reminders",
|
||||
"ui/locale",
|
||||
"ui/font_size",
|
||||
]:
|
||||
s.remove(k)
|
||||
|
||||
|
|
@ -29,7 +33,11 @@ def test_load_and_save_db_config_roundtrip(app, tmp_path):
|
|||
idle_minutes=7,
|
||||
theme="dark",
|
||||
move_todos=True,
|
||||
tags=True,
|
||||
time_log=True,
|
||||
reminders=True,
|
||||
locale="en",
|
||||
font_size=11,
|
||||
)
|
||||
save_db_config(cfg)
|
||||
|
||||
|
|
@ -39,7 +47,11 @@ def test_load_and_save_db_config_roundtrip(app, tmp_path):
|
|||
assert loaded.idle_minutes == cfg.idle_minutes
|
||||
assert loaded.theme == cfg.theme
|
||||
assert loaded.move_todos == cfg.move_todos
|
||||
assert loaded.tags == cfg.tags
|
||||
assert loaded.time_log == cfg.time_log
|
||||
assert loaded.reminders == cfg.reminders
|
||||
assert loaded.locale == cfg.locale
|
||||
assert loaded.font_size == cfg.font_size
|
||||
|
||||
|
||||
def test_load_db_config_migrates_legacy_db_path(app, tmp_path):
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db):
|
|||
dlg.move_todos.setChecked(True)
|
||||
dlg.tags.setChecked(False)
|
||||
dlg.time_log.setChecked(False)
|
||||
dlg.reminders.setChecked(False)
|
||||
|
||||
# Auto-accept the modal QMessageBox that _compact_btn_clicked() shows
|
||||
def _auto_accept_msgbox():
|
||||
|
|
@ -39,6 +40,7 @@ def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db):
|
|||
assert cfg.move_todos is True
|
||||
assert cfg.tags is False
|
||||
assert cfg.time_log is False
|
||||
assert cfg.reminders is False
|
||||
assert cfg.theme in ("light", "dark", "system")
|
||||
|
||||
|
||||
|
|
|
|||
384
tests/test_table_editor.py
Normal file
384
tests/test_table_editor.py
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
from bouquin.table_editor import TableEditorDialog, find_table_at_cursor, _is_table_line
|
||||
|
||||
|
||||
def test_table_editor_init_simple_table(qtbot, app):
|
||||
"""Test initialization with a simple markdown table."""
|
||||
table_text = """| Header1 | Header2 | Header3 |
|
||||
| --- | --- | --- |
|
||||
| Cell1 | Cell2 | Cell3 |
|
||||
| Cell4 | Cell5 | Cell6 |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.table_widget.columnCount() == 3
|
||||
assert dialog.table_widget.rowCount() == 2
|
||||
assert dialog.table_widget.horizontalHeaderItem(0).text() == "Header1"
|
||||
assert dialog.table_widget.horizontalHeaderItem(1).text() == "Header2"
|
||||
assert dialog.table_widget.item(0, 0).text() == "Cell1"
|
||||
assert dialog.table_widget.item(1, 2).text() == "Cell6"
|
||||
|
||||
|
||||
def test_table_editor_no_separator_line(qtbot, app):
|
||||
"""Test parsing table without separator line."""
|
||||
table_text = """| Header1 | Header2 |
|
||||
| Cell1 | Cell2 |
|
||||
| Cell3 | Cell4 |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.table_widget.columnCount() == 2
|
||||
assert dialog.table_widget.rowCount() == 2
|
||||
assert dialog.table_widget.item(0, 0).text() == "Cell1"
|
||||
|
||||
|
||||
def test_table_editor_empty_table(qtbot, app):
|
||||
"""Test initialization with empty table text."""
|
||||
dialog = TableEditorDialog("")
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should have no columns/rows
|
||||
assert dialog.table_widget.columnCount() == 0 or dialog.table_widget.rowCount() == 0
|
||||
|
||||
|
||||
def test_table_editor_single_header_line(qtbot, app):
|
||||
"""Test table with only header line."""
|
||||
table_text = "| Header1 | Header2 | Header3 |"
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.table_widget.columnCount() == 3
|
||||
assert dialog.table_widget.rowCount() == 0
|
||||
|
||||
|
||||
def test_add_row(qtbot, app):
|
||||
"""Test adding a row to the table."""
|
||||
table_text = """| H1 | H2 |
|
||||
| --- | --- |
|
||||
| A | B |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
initial_rows = dialog.table_widget.rowCount()
|
||||
dialog._add_row()
|
||||
|
||||
assert dialog.table_widget.rowCount() == initial_rows + 1
|
||||
# New row should have empty items
|
||||
assert dialog.table_widget.item(initial_rows, 0).text() == ""
|
||||
assert dialog.table_widget.item(initial_rows, 1).text() == ""
|
||||
|
||||
|
||||
def test_add_column(qtbot, app):
|
||||
"""Test adding a column to the table."""
|
||||
table_text = """| H1 | H2 |
|
||||
| --- | --- |
|
||||
| A | B |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
initial_cols = dialog.table_widget.columnCount()
|
||||
dialog._add_column()
|
||||
|
||||
assert dialog.table_widget.columnCount() == initial_cols + 1
|
||||
# New column should have header and empty items
|
||||
assert "Column" in dialog.table_widget.horizontalHeaderItem(initial_cols).text()
|
||||
assert dialog.table_widget.item(0, initial_cols).text() == ""
|
||||
|
||||
|
||||
def test_delete_row(qtbot, app):
|
||||
"""Test deleting a row from the table."""
|
||||
table_text = """| H1 | H2 |
|
||||
| --- | --- |
|
||||
| A | B |
|
||||
| C | D |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
initial_rows = dialog.table_widget.rowCount()
|
||||
dialog.table_widget.setCurrentCell(0, 0)
|
||||
dialog._delete_row()
|
||||
|
||||
assert dialog.table_widget.rowCount() == initial_rows - 1
|
||||
|
||||
|
||||
def test_delete_row_no_selection(qtbot, app):
|
||||
"""Test deleting a row when nothing is selected."""
|
||||
table_text = """| H1 | H2 |
|
||||
| --- | --- |
|
||||
| A | B |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
initial_rows = dialog.table_widget.rowCount()
|
||||
dialog.table_widget.setCurrentCell(-1, -1) # No selection
|
||||
dialog._delete_row()
|
||||
|
||||
# Row count should remain the same
|
||||
assert dialog.table_widget.rowCount() == initial_rows
|
||||
|
||||
|
||||
def test_delete_column(qtbot, app):
|
||||
"""Test deleting a column from the table."""
|
||||
table_text = """| H1 | H2 | H3 |
|
||||
| --- | --- | --- |
|
||||
| A | B | C |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
initial_cols = dialog.table_widget.columnCount()
|
||||
dialog.table_widget.setCurrentCell(0, 1)
|
||||
dialog._delete_column()
|
||||
|
||||
assert dialog.table_widget.columnCount() == initial_cols - 1
|
||||
|
||||
|
||||
def test_delete_column_no_selection(qtbot, app):
|
||||
"""Test deleting a column when nothing is selected."""
|
||||
table_text = """| H1 | H2 |
|
||||
| --- | --- |
|
||||
| A | B |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
initial_cols = dialog.table_widget.columnCount()
|
||||
dialog.table_widget.setCurrentCell(-1, -1) # No selection
|
||||
dialog._delete_column()
|
||||
|
||||
# Column count should remain the same
|
||||
assert dialog.table_widget.columnCount() == initial_cols
|
||||
|
||||
|
||||
def test_get_markdown_table(qtbot, app):
|
||||
"""Test converting table back to markdown."""
|
||||
table_text = """| Name | Age | City |
|
||||
| --- | --- | --- |
|
||||
| Alice | 30 | NYC |
|
||||
| Bob | 25 | LA |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
result = dialog.get_markdown_table()
|
||||
|
||||
assert "| Name | Age | City |" in result
|
||||
assert "| --- | --- | --- |" in result
|
||||
assert "| Alice | 30 | NYC |" in result
|
||||
assert "| Bob | 25 | LA |" in result
|
||||
|
||||
|
||||
def test_get_markdown_table_empty(qtbot, app):
|
||||
"""Test getting markdown from empty table."""
|
||||
dialog = TableEditorDialog("")
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
result = dialog.get_markdown_table()
|
||||
assert result == ""
|
||||
|
||||
|
||||
def test_get_markdown_table_with_modifications(qtbot, app):
|
||||
"""Test getting markdown after modifying table."""
|
||||
table_text = """| H1 | H2 |
|
||||
| --- | --- |
|
||||
| A | B |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Modify a cell
|
||||
dialog.table_widget.item(0, 0).setText("Modified")
|
||||
|
||||
result = dialog.get_markdown_table()
|
||||
assert "Modified" in result
|
||||
|
||||
|
||||
def test_find_table_at_cursor_middle_of_table(qtbot, app):
|
||||
"""Test finding table when cursor is in the middle."""
|
||||
text = """Some text before
|
||||
|
||||
| H1 | H2 |
|
||||
| --- | --- |
|
||||
| A | B |
|
||||
| C | D |
|
||||
|
||||
Some text after"""
|
||||
|
||||
# Cursor position in the middle of the table
|
||||
cursor_pos = text.find("| A |") + 2
|
||||
result = find_table_at_cursor(text, cursor_pos)
|
||||
|
||||
assert result is not None
|
||||
start, end, table_text = result
|
||||
assert "| H1 | H2 |" in table_text
|
||||
assert "| A | B |" in table_text
|
||||
assert "Some text before" not in table_text
|
||||
assert "Some text after" not in table_text
|
||||
|
||||
|
||||
def test_find_table_at_cursor_first_line(qtbot, app):
|
||||
"""Test finding table when cursor is on the first line."""
|
||||
text = """| H1 | H2 |
|
||||
| --- | --- |
|
||||
| A | B |"""
|
||||
|
||||
cursor_pos = 5 # In the first line
|
||||
result = find_table_at_cursor(text, cursor_pos)
|
||||
|
||||
assert result is not None
|
||||
start, end, table_text = result
|
||||
assert "| H1 | H2 |" in table_text
|
||||
|
||||
|
||||
def test_find_table_at_cursor_not_in_table(qtbot, app):
|
||||
"""Test finding table when cursor is not in a table."""
|
||||
text = """Just some regular text
|
||||
No tables here
|
||||
|
||||
| H1 | H2 |
|
||||
| --- | --- |
|
||||
| A | B |"""
|
||||
|
||||
cursor_pos = 10 # In "Just some regular text"
|
||||
result = find_table_at_cursor(text, cursor_pos)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_find_table_at_cursor_empty_text(qtbot, app):
|
||||
"""Test finding table in empty text."""
|
||||
result = find_table_at_cursor("", 0)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_find_table_at_cursor_multiple_tables(qtbot, app):
|
||||
"""Test finding correct table when there are multiple tables."""
|
||||
text = """| Table1 | H1 |
|
||||
| --- | --- |
|
||||
|
||||
Some text
|
||||
|
||||
| Table2 | H2 |
|
||||
| --- | --- |
|
||||
| Data | Here |"""
|
||||
|
||||
# Cursor in second table
|
||||
cursor_pos = text.find("| Data |")
|
||||
result = find_table_at_cursor(text, cursor_pos)
|
||||
|
||||
assert result is not None
|
||||
start, end, table_text = result
|
||||
assert "Table2" in table_text
|
||||
assert "Table1" not in table_text
|
||||
|
||||
|
||||
def test_is_table_line_valid(qtbot, app):
|
||||
"""Test identifying valid table lines."""
|
||||
assert _is_table_line("| Header | Header2 |") is True
|
||||
assert _is_table_line("| --- | --- |") is True
|
||||
assert _is_table_line("| Cell | Cell2 | Cell3 |") is True
|
||||
|
||||
|
||||
def test_is_table_line_invalid(qtbot, app):
|
||||
"""Test identifying invalid table lines."""
|
||||
assert _is_table_line("Just regular text") is False
|
||||
assert _is_table_line("") is False
|
||||
assert _is_table_line(" ") is False
|
||||
assert _is_table_line("| Only one pipe") is False
|
||||
assert _is_table_line("Only one pipe |") is False
|
||||
assert _is_table_line("No pipes at all") is False
|
||||
|
||||
|
||||
def test_is_table_line_edge_cases(qtbot, app):
|
||||
"""Test edge cases for table line detection."""
|
||||
assert _is_table_line("| | |") is True # Minimal valid table
|
||||
assert (
|
||||
_is_table_line(" | Header | Data | ") is True
|
||||
) # With leading/trailing spaces
|
||||
|
||||
|
||||
def test_table_with_alignment_indicators(qtbot, app):
|
||||
"""Test parsing table with alignment indicators."""
|
||||
table_text = """| Left | Center | Right |
|
||||
| :--- | :---: | ---: |
|
||||
| L1 | C1 | R1 |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.table_widget.columnCount() == 3
|
||||
assert dialog.table_widget.rowCount() == 1
|
||||
assert dialog.table_widget.item(0, 0).text() == "L1"
|
||||
|
||||
|
||||
def test_accept_dialog(qtbot, app):
|
||||
"""Test accepting the dialog."""
|
||||
table_text = "| H1 | H2 |\n| --- | --- |\n| A | B |"
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Find and click the OK button
|
||||
for child in dialog.findChildren(type(dialog.findChild(type(None)))):
|
||||
if hasattr(child, "text") and callable(child.text):
|
||||
try:
|
||||
if "ok" in child.text().lower() or "OK" in child.text():
|
||||
child.click()
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def test_reject_dialog(qtbot, app):
|
||||
"""Test rejecting the dialog."""
|
||||
table_text = "| H1 | H2 |\n| --- | --- |\n| A | B |"
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Find and click the Cancel button
|
||||
for child in dialog.findChildren(type(dialog.findChild(type(None)))):
|
||||
if hasattr(child, "text") and callable(child.text):
|
||||
try:
|
||||
if "cancel" in child.text().lower():
|
||||
child.click()
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def test_table_with_uneven_columns(qtbot, app):
|
||||
"""Test parsing table with uneven number of columns in rows."""
|
||||
table_text = """| H1 | H2 | H3 |
|
||||
| --- | --- | --- |
|
||||
| A | B |
|
||||
| C | D | E | F |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should handle gracefully
|
||||
assert dialog.table_widget.columnCount() == 3
|
||||
assert dialog.table_widget.rowCount() == 2
|
||||
|
||||
|
||||
def test_table_with_empty_cells(qtbot, app):
|
||||
"""Test parsing table with empty cells."""
|
||||
table_text = """| H1 | H2 | H3 |
|
||||
| --- | --- | --- |
|
||||
| | B | |
|
||||
| C | | E |"""
|
||||
|
||||
dialog = TableEditorDialog(table_text)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.table_widget.item(0, 0).text() == ""
|
||||
assert dialog.table_widget.item(0, 1).text() == "B"
|
||||
assert dialog.table_widget.item(0, 2).text() == ""
|
||||
assert dialog.table_widget.item(1, 0).text() == "C"
|
||||
assert dialog.table_widget.item(1, 1).text() == ""
|
||||
assert dialog.table_widget.item(1, 2).text() == "E"
|
||||
512
tests/test_version_check.py
Normal file
512
tests/test_version_check.py
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
import subprocess
|
||||
from bouquin.version_check import VersionChecker
|
||||
from PySide6.QtWidgets import QMessageBox, QWidget
|
||||
from PySide6.QtGui import QPixmap
|
||||
|
||||
|
||||
def test_version_checker_init(app):
|
||||
"""Test VersionChecker initialization."""
|
||||
parent = QWidget()
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
assert checker._parent is parent
|
||||
|
||||
|
||||
def test_version_checker_init_no_parent(app):
|
||||
"""Test VersionChecker initialization without parent."""
|
||||
checker = VersionChecker()
|
||||
|
||||
assert checker._parent is None
|
||||
|
||||
|
||||
def test_current_version_returns_version(app):
|
||||
"""Test getting current version."""
|
||||
checker = VersionChecker()
|
||||
|
||||
with patch("importlib.metadata.version", return_value="1.2.3"):
|
||||
version = checker.current_version()
|
||||
assert version == "1.2.3"
|
||||
|
||||
|
||||
def test_current_version_fallback_on_error(app):
|
||||
"""Test current version fallback when package not found."""
|
||||
checker = VersionChecker()
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
with patch(
|
||||
"importlib.metadata.version",
|
||||
side_effect=importlib.metadata.PackageNotFoundError("Not found"),
|
||||
):
|
||||
version = checker.current_version()
|
||||
assert version == "0.0.0"
|
||||
|
||||
|
||||
def test_parse_version_simple(app):
|
||||
"""Test parsing simple version string."""
|
||||
result = VersionChecker._parse_version("1.2.3")
|
||||
assert result == (1, 2, 3)
|
||||
|
||||
|
||||
def test_parse_version_complex(app):
|
||||
"""Test parsing complex version string with extra text."""
|
||||
result = VersionChecker._parse_version("v1.2.3-beta")
|
||||
assert result == (1, 2, 3)
|
||||
|
||||
|
||||
def test_parse_version_no_numbers(app):
|
||||
"""Test parsing version string with no numbers."""
|
||||
result = VersionChecker._parse_version("invalid")
|
||||
assert result == (0,)
|
||||
|
||||
|
||||
def test_parse_version_single_number(app):
|
||||
"""Test parsing version with single number."""
|
||||
result = VersionChecker._parse_version("5")
|
||||
assert result == (5,)
|
||||
|
||||
|
||||
def test_is_newer_version_true(app):
|
||||
"""Test detecting newer version."""
|
||||
checker = VersionChecker()
|
||||
|
||||
assert checker._is_newer_version("1.2.3", "1.2.2") is True
|
||||
assert checker._is_newer_version("2.0.0", "1.9.9") is True
|
||||
assert checker._is_newer_version("1.3.0", "1.2.9") is True
|
||||
|
||||
|
||||
def test_is_newer_version_false(app):
|
||||
"""Test detecting same or older version."""
|
||||
checker = VersionChecker()
|
||||
|
||||
assert checker._is_newer_version("1.2.3", "1.2.3") is False
|
||||
assert checker._is_newer_version("1.2.2", "1.2.3") is False
|
||||
assert checker._is_newer_version("0.9.9", "1.0.0") is False
|
||||
|
||||
|
||||
def test_logo_pixmap(app):
|
||||
"""Test generating logo pixmap."""
|
||||
checker = VersionChecker()
|
||||
|
||||
pixmap = checker._logo_pixmap(96)
|
||||
|
||||
assert isinstance(pixmap, QPixmap)
|
||||
assert not pixmap.isNull()
|
||||
|
||||
|
||||
def test_logo_pixmap_different_sizes(app):
|
||||
"""Test generating logo pixmap with different sizes."""
|
||||
checker = VersionChecker()
|
||||
|
||||
pixmap_small = checker._logo_pixmap(48)
|
||||
pixmap_large = checker._logo_pixmap(128)
|
||||
|
||||
assert not pixmap_small.isNull()
|
||||
assert not pixmap_large.isNull()
|
||||
|
||||
|
||||
def test_show_version_dialog(qtbot, app):
|
||||
"""Test showing version dialog."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
with patch.object(QMessageBox, "exec") as mock_exec:
|
||||
with patch("importlib.metadata.version", return_value="1.0.0"):
|
||||
checker.show_version_dialog()
|
||||
|
||||
# Dialog should have been shown
|
||||
assert mock_exec.called
|
||||
|
||||
|
||||
def test_check_for_updates_network_error(qtbot, app):
|
||||
"""Test check for updates when network request fails."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
with patch("requests.get", side_effect=Exception("Network error")):
|
||||
with patch.object(QMessageBox, "warning") as mock_warning:
|
||||
checker.check_for_updates()
|
||||
|
||||
# Should show warning
|
||||
assert mock_warning.called
|
||||
|
||||
|
||||
def test_check_for_updates_empty_response(qtbot, app):
|
||||
"""Test check for updates with empty version string."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.text = " "
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
with patch.object(QMessageBox, "warning") as mock_warning:
|
||||
checker.check_for_updates()
|
||||
|
||||
# Should show warning about empty version
|
||||
assert mock_warning.called
|
||||
|
||||
|
||||
def test_check_for_updates_already_latest(qtbot, app):
|
||||
"""Test check for updates when already on latest version."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.text = "1.0.0"
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
with patch("importlib.metadata.version", return_value="1.0.0"):
|
||||
with patch.object(QMessageBox, "information") as mock_info:
|
||||
checker.check_for_updates()
|
||||
|
||||
# Should show info that we're on latest
|
||||
assert mock_info.called
|
||||
|
||||
|
||||
def test_check_for_updates_new_version_available_declined(qtbot, app):
|
||||
"""Test check for updates when new version is available but user declines."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.text = "2.0.0"
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
with patch("importlib.metadata.version", return_value="1.0.0"):
|
||||
with patch.object(QMessageBox, "question", return_value=QMessageBox.No):
|
||||
# Should not proceed to download
|
||||
checker.check_for_updates()
|
||||
|
||||
|
||||
def test_check_for_updates_new_version_available_accepted(qtbot, app):
|
||||
"""Test check for updates when new version is available and user accepts."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.text = "2.0.0"
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
with patch("importlib.metadata.version", return_value="1.0.0"):
|
||||
with patch.object(QMessageBox, "question", return_value=QMessageBox.Yes):
|
||||
with patch.object(
|
||||
checker, "_download_and_verify_appimage"
|
||||
) as mock_download:
|
||||
checker.check_for_updates()
|
||||
|
||||
# Should call download
|
||||
mock_download.assert_called_once_with("2.0.0")
|
||||
|
||||
|
||||
def test_download_file_success(qtbot, app, tmp_path):
|
||||
"""Test downloading a file successfully."""
|
||||
checker = VersionChecker()
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.headers = {"Content-Length": "1000"}
|
||||
mock_response.iter_content = Mock(return_value=[b"data" * 25]) # 100 bytes
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
dest_path = tmp_path / "test_file.bin"
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
checker._download_file("http://example.com/file", dest_path)
|
||||
|
||||
assert dest_path.exists()
|
||||
|
||||
|
||||
def test_download_file_with_progress(qtbot, app, tmp_path):
|
||||
"""Test downloading a file with progress dialog."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.headers = {"Content-Length": "1000"}
|
||||
mock_response.iter_content = Mock(return_value=[b"x" * 100, b"y" * 100])
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
dest_path = tmp_path / "test_file.bin"
|
||||
|
||||
from PySide6.QtWidgets import QProgressDialog
|
||||
|
||||
mock_progress = Mock(spec=QProgressDialog)
|
||||
mock_progress.wasCanceled = Mock(return_value=False)
|
||||
mock_progress.value = Mock(return_value=0)
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
checker._download_file(
|
||||
"http://example.com/file", dest_path, progress=mock_progress
|
||||
)
|
||||
|
||||
# Progress should have been updated
|
||||
assert mock_progress.setValue.called
|
||||
|
||||
|
||||
def test_download_file_cancelled(qtbot, app, tmp_path):
|
||||
"""Test cancelling a file download."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.headers = {"Content-Length": "1000"}
|
||||
mock_response.iter_content = Mock(return_value=[b"x" * 100])
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
dest_path = tmp_path / "test_file.bin"
|
||||
|
||||
from PySide6.QtWidgets import QProgressDialog
|
||||
|
||||
mock_progress = Mock(spec=QProgressDialog)
|
||||
mock_progress.wasCanceled = Mock(return_value=True)
|
||||
mock_progress.value = Mock(return_value=0)
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
with pytest.raises(RuntimeError):
|
||||
checker._download_file(
|
||||
"http://example.com/file", dest_path, progress=mock_progress
|
||||
)
|
||||
|
||||
|
||||
def test_download_file_no_content_length(qtbot, app, tmp_path):
|
||||
"""Test downloading file without Content-Length header."""
|
||||
checker = VersionChecker()
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.headers = {}
|
||||
mock_response.iter_content = Mock(return_value=[b"data"])
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
dest_path = tmp_path / "test_file.bin"
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
checker._download_file("http://example.com/file", dest_path)
|
||||
|
||||
assert dest_path.exists()
|
||||
|
||||
|
||||
def test_download_and_verify_appimage_download_cancelled(qtbot, app, tmp_path):
|
||||
"""Test AppImage download when user cancels."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
with patch(
|
||||
"bouquin.version_check.QStandardPaths.writableLocation",
|
||||
return_value=str(tmp_path),
|
||||
):
|
||||
with patch.object(
|
||||
checker, "_download_file", side_effect=RuntimeError("Download cancelled")
|
||||
):
|
||||
with patch.object(QMessageBox, "information") as mock_info:
|
||||
checker._download_and_verify_appimage("2.0.0")
|
||||
|
||||
# Should show cancellation message
|
||||
assert mock_info.called
|
||||
|
||||
|
||||
def test_download_and_verify_appimage_download_error(qtbot, app, tmp_path):
|
||||
"""Test AppImage download when download fails."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
with patch(
|
||||
"bouquin.version_check.QStandardPaths.writableLocation",
|
||||
return_value=str(tmp_path),
|
||||
):
|
||||
with patch.object(
|
||||
checker, "_download_file", side_effect=Exception("Network error")
|
||||
):
|
||||
with patch.object(QMessageBox, "critical") as mock_critical:
|
||||
checker._download_and_verify_appimage("2.0.0")
|
||||
|
||||
# Should show error message
|
||||
assert mock_critical.called
|
||||
|
||||
|
||||
def test_download_and_verify_appimage_gpg_key_error(qtbot, app, tmp_path):
|
||||
"""Test AppImage verification when GPG key cannot be read."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
with patch(
|
||||
"bouquin.version_check.QStandardPaths.writableLocation",
|
||||
return_value=str(tmp_path),
|
||||
):
|
||||
with patch.object(checker, "_download_file"):
|
||||
with patch(
|
||||
"importlib.resources.files", side_effect=Exception("Key not found")
|
||||
):
|
||||
with patch.object(QMessageBox, "critical") as mock_critical:
|
||||
checker._download_and_verify_appimage("2.0.0")
|
||||
|
||||
# Should show error about GPG key
|
||||
assert mock_critical.called
|
||||
|
||||
|
||||
def test_download_and_verify_appimage_gpg_not_found(qtbot, app, tmp_path):
|
||||
"""Test AppImage verification when GPG is not installed."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_files = Mock()
|
||||
mock_files.read_bytes = Mock(return_value=b"fake key data")
|
||||
|
||||
with patch(
|
||||
"bouquin.version_check.QStandardPaths.writableLocation",
|
||||
return_value=str(tmp_path),
|
||||
):
|
||||
with patch.object(checker, "_download_file"):
|
||||
with patch("importlib.resources.files", return_value=mock_files):
|
||||
with patch(
|
||||
"subprocess.run", side_effect=FileNotFoundError("gpg not found")
|
||||
):
|
||||
with patch.object(QMessageBox, "critical") as mock_critical:
|
||||
checker._download_and_verify_appimage("2.0.0")
|
||||
|
||||
# Should show error about GPG not found
|
||||
assert mock_critical.called
|
||||
|
||||
|
||||
def test_download_and_verify_appimage_verification_failed(qtbot, app, tmp_path):
|
||||
"""Test AppImage verification when signature verification fails."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_files = Mock()
|
||||
mock_files.read_bytes = Mock(return_value=b"fake key data")
|
||||
|
||||
with patch(
|
||||
"bouquin.version_check.QStandardPaths.writableLocation",
|
||||
return_value=str(tmp_path),
|
||||
):
|
||||
with patch.object(checker, "_download_file"):
|
||||
with patch("importlib.resources.files", return_value=mock_files):
|
||||
# First subprocess call (import) succeeds, second (verify) fails
|
||||
mock_error = subprocess.CalledProcessError(1, "gpg")
|
||||
mock_error.stderr = b"Verification failed"
|
||||
with patch("subprocess.run", side_effect=[None, mock_error]):
|
||||
with patch.object(QMessageBox, "critical") as mock_critical:
|
||||
checker._download_and_verify_appimage("2.0.0")
|
||||
|
||||
# Should show error about verification
|
||||
assert mock_critical.called
|
||||
|
||||
|
||||
def test_download_and_verify_appimage_success(qtbot, app, tmp_path):
|
||||
"""Test successful AppImage download and verification."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_files = Mock()
|
||||
mock_files.read_bytes = Mock(return_value=b"fake key data")
|
||||
|
||||
with patch(
|
||||
"bouquin.version_check.QStandardPaths.writableLocation",
|
||||
return_value=str(tmp_path),
|
||||
):
|
||||
with patch.object(checker, "_download_file"):
|
||||
with patch("importlib.resources.files", return_value=mock_files):
|
||||
with patch("subprocess.run"): # Both calls succeed
|
||||
with patch.object(QMessageBox, "information") as mock_info:
|
||||
checker._download_and_verify_appimage("2.0.0")
|
||||
|
||||
# Should show success message
|
||||
assert mock_info.called
|
||||
|
||||
|
||||
def test_version_comparison_edge_cases(app):
|
||||
"""Test version comparison with edge cases."""
|
||||
checker = VersionChecker()
|
||||
|
||||
# Different lengths
|
||||
assert checker._is_newer_version("1.0.0.1", "1.0.0") is True
|
||||
assert checker._is_newer_version("1.0", "1.0.0") is False
|
||||
|
||||
# Large numbers
|
||||
assert checker._is_newer_version("10.0.0", "9.9.9") is True
|
||||
assert checker._is_newer_version("1.100.0", "1.99.0") is True
|
||||
|
||||
|
||||
def test_download_file_creates_parent_directory(qtbot, app, tmp_path):
|
||||
"""Test that download creates parent directory if needed."""
|
||||
checker = VersionChecker()
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.headers = {}
|
||||
mock_response.iter_content = Mock(return_value=[b"data"])
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
dest_path = tmp_path / "subdir" / "nested" / "test_file.bin"
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
checker._download_file("http://example.com/file", dest_path)
|
||||
|
||||
assert dest_path.exists()
|
||||
assert dest_path.parent.exists()
|
||||
|
||||
|
||||
def test_show_version_dialog_check_button_clicked(qtbot, app):
|
||||
"""Test clicking 'Check for updates' button in version dialog."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_box = Mock(spec=QMessageBox)
|
||||
check_button = Mock()
|
||||
mock_box.clickedButton = Mock(return_value=check_button)
|
||||
mock_box.addButton = Mock(return_value=check_button)
|
||||
|
||||
with patch("importlib.metadata.version", return_value="1.0.0"):
|
||||
with patch("bouquin.version_check.QMessageBox", return_value=mock_box):
|
||||
with patch.object(checker, "check_for_updates") as mock_check:
|
||||
checker.show_version_dialog()
|
||||
|
||||
# check_for_updates should be called when button is clicked
|
||||
if mock_box.clickedButton() is check_button:
|
||||
assert mock_check.called
|
||||
|
||||
|
||||
def test_parse_version_with_letters(app):
|
||||
"""Test parsing version strings with letters."""
|
||||
result = VersionChecker._parse_version("1.2.3rc1")
|
||||
assert 1 in result
|
||||
assert 2 in result
|
||||
assert 3 in result
|
||||
|
||||
|
||||
def test_download_file_invalid_content_length(qtbot, app, tmp_path):
|
||||
"""Test downloading file with invalid Content-Length header."""
|
||||
checker = VersionChecker()
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.headers = {"Content-Length": "invalid"}
|
||||
mock_response.iter_content = Mock(return_value=[b"data"])
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
dest_path = tmp_path / "test_file.bin"
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
# Should handle gracefully
|
||||
checker._download_file("http://example.com/file", dest_path)
|
||||
|
||||
assert dest_path.exists()
|
||||
Loading…
Add table
Add a link
Reference in a new issue