From ca3c839c7d2b509135e2cc4bdfe63f60c6874d2f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 21 Nov 2025 14:30:38 +1100 Subject: [PATCH] More tests --- tests/test_key_prompt.py | 196 ++++++++++++++ tests/test_main_window.py | 276 +++++++++++++++++++ tests/test_markdown_editor.py | 451 ++++++++++++++++++++++++++++++++ tests/test_statistics_dialog.py | 100 ++++++- tests/test_tags.py | 164 +++++++++++- 5 files changed, 1184 insertions(+), 3 deletions(-) diff --git a/tests/test_key_prompt.py b/tests/test_key_prompt.py index 32db6d9..f044fac 100644 --- a/tests/test_key_prompt.py +++ b/tests/test_key_prompt.py @@ -1,5 +1,8 @@ from bouquin.key_prompt import KeyPrompt +from PySide6.QtCore import QTimer +from PySide6.QtWidgets import QFileDialog, QLineEdit + def test_key_prompt_roundtrip(qtbot): kp = KeyPrompt() @@ -7,3 +10,196 @@ def test_key_prompt_roundtrip(qtbot): kp.show() kp.key_entry.setText("swordfish") assert kp.key() == "swordfish" + + +def test_key_prompt_with_db_path_browse(qtbot, app, tmp_path, monkeypatch): + """Test KeyPrompt with DB path selection - covers lines 57-67""" + test_db = tmp_path / "test.db" + test_db.touch() + + # Create prompt with show_db_change=True + prompt = KeyPrompt(show_db_change=True) + qtbot.addWidget(prompt) + + # Mock the file dialog to return a file + def mock_get_open_filename(*args, **kwargs): + return str(test_db), "SQLCipher DB (*.db)" + + monkeypatch.setattr(QFileDialog, "getOpenFileName", mock_get_open_filename) + + # Simulate clicking the browse button + # Find the browse button by looking through the widget's children + browse_btn = None + for child in prompt.findChildren(object): + if hasattr(child, "clicked") and hasattr(child, "text"): + if ( + "select" in str(child.text()).lower() + or "browse" in str(child.text()).lower() + ): + browse_btn = child + break + + if browse_btn: + browse_btn.click() + qtbot.wait(50) + + # Verify the path was set + assert prompt.path_edit is not None + assert str(test_db) in prompt.path_edit.text() + + +def test_key_prompt_with_db_path_no_file_selected(qtbot, app, tmp_path, monkeypatch): + """Test KeyPrompt when cancel is clicked in file dialog - covers line 64 condition""" + # Create prompt with show_db_change=True + prompt = KeyPrompt(show_db_change=True) + qtbot.addWidget(prompt) + + # Mock the file dialog to return empty string (user cancelled) + def mock_get_open_filename(*args, **kwargs): + return "", "" + + monkeypatch.setattr(QFileDialog, "getOpenFileName", mock_get_open_filename) + + # Store original path text + original_text = prompt.path_edit.text() if prompt.path_edit else "" + + # Simulate clicking the browse button + browse_btn = None + for child in prompt.findChildren(object): + if hasattr(child, "clicked") and hasattr(child, "text"): + if ( + "select" in str(child.text()).lower() + or "browse" in str(child.text()).lower() + ): + browse_btn = child + break + + if browse_btn: + browse_btn.click() + qtbot.wait(50) + + # Path should not have changed since no file was selected + if prompt.path_edit: + assert prompt.path_edit.text() == original_text + + +def test_key_prompt_with_existing_db_path(qtbot, app, tmp_path): + """Test KeyPrompt with existing DB path provided""" + test_db = tmp_path / "existing.db" + test_db.touch() + + prompt = KeyPrompt(show_db_change=True, initial_db_path=test_db) + qtbot.addWidget(prompt) + + # Verify the path is pre-filled + assert prompt.path_edit is not None + assert str(test_db) in prompt.path_edit.text() + + +def test_key_prompt_with_db_path_none_and_show_db_change(qtbot, app): + """Test KeyPrompt with show_db_change but no initial_db_path - covers line 57""" + prompt = KeyPrompt(show_db_change=True, initial_db_path=None) + qtbot.addWidget(prompt) + + # Path edit should exist but be empty + assert prompt.path_edit is not None + assert prompt.path_edit.text() == "" + + +def test_key_prompt_accept_with_valid_key(qtbot, app): + """Test accepting prompt with valid key""" + prompt = KeyPrompt() + qtbot.addWidget(prompt) + + # Enter a key + prompt.key_entry.setText("test-key-123") + + # Accept + QTimer.singleShot(0, prompt.accept) + qtbot.wait(50) + + assert prompt.key_entry.text() == "test-key-123" + + +def test_key_prompt_without_db_change(qtbot, app): + """Test KeyPrompt without show_db_change""" + prompt = KeyPrompt(show_db_change=False) + qtbot.addWidget(prompt) + + # Path edit should not exist + assert prompt.path_edit is None + + +def test_key_prompt_password_visibility(qtbot, app): + """Test password entry mode""" + prompt = KeyPrompt() + qtbot.addWidget(prompt) + + # Initially should be password mode + assert prompt.key_entry.echoMode() == QLineEdit.EchoMode.Password + + # Enter some text + prompt.key_entry.setText("secret") + + # The text should be obscured + assert prompt.key_entry.echoMode() == QLineEdit.EchoMode.Password + + +def test_key_prompt_key_method(qtbot, app): + """Test the key() method returns entered text""" + prompt = KeyPrompt() + qtbot.addWidget(prompt) + + prompt.key_entry.setText("my-secret-key") + + assert prompt.key() == "my-secret-key" + + +def test_key_prompt_db_path_method(qtbot, app, tmp_path): + """Test the db_path() method returns selected path""" + test_db = tmp_path / "test.db" + test_db.touch() + + prompt = KeyPrompt(show_db_change=True, initial_db_path=test_db) + qtbot.addWidget(prompt) + + # Should return the db_path + assert prompt.db_path() == test_db + + +def test_key_prompt_browse_with_initial_path(qtbot, app, tmp_path, monkeypatch): + """Test browsing when initial_db_path is set - covers line 57 with non-None path""" + initial_db = tmp_path / "initial.db" + initial_db.touch() + + new_db = tmp_path / "new.db" + new_db.touch() + + prompt = KeyPrompt(show_db_change=True, initial_db_path=initial_db) + qtbot.addWidget(prompt) + + # Mock the file dialog to return a different file + def mock_get_open_filename(*args, **kwargs): + # Verify that start_dir was passed correctly (line 57) + return str(new_db), "SQLCipher DB (*.db)" + + monkeypatch.setattr(QFileDialog, "getOpenFileName", mock_get_open_filename) + + # Find and click browse button + browse_btn = None + for child in prompt.findChildren(object): + if hasattr(child, "clicked") and hasattr(child, "text"): + if ( + "select" in str(child.text()).lower() + or "browse" in str(child.text()).lower() + ): + browse_btn = child + break + + if browse_btn: + browse_btn.click() + qtbot.wait(50) + + # Verify new path was set + assert str(new_db) in prompt.path_edit.text() + assert prompt.db_path() == new_db diff --git a/tests/test_main_window.py b/tests/test_main_window.py index 4cdc298..58f7753 100644 --- a/tests/test_main_window.py +++ b/tests/test_main_window.py @@ -1812,3 +1812,279 @@ def test_main_window_update_tag_views_no_tags_widget( window._update_tag_views_for_date("2024-01-15") assert True + + +def test_main_window_with_tags_disabled(qtbot, app, tmp_path): + """Test MainWindow with tags disabled in config - covers line 319""" + db_path = tmp_path / "notebook.db" + s = get_settings() + s.setValue("db/default_db", str(db_path)) + s.setValue("db/key", "test-key") + s.setValue("ui/idle_minutes", 0) + s.setValue("ui/theme", "light") + s.setValue("ui/tags", False) # Disable tags + s.setValue("ui/time_log", True) + + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + w = MainWindow(themes=themes) + qtbot.addWidget(w) + w.show() + + # Tags widget should be hidden + assert w.tags.isHidden() + + +def test_main_window_with_time_log_disabled(qtbot, app, tmp_path): + """Test MainWindow with time_log disabled in config - covers line 321""" + db_path = tmp_path / "notebook.db" + s = get_settings() + s.setValue("db/default_db", str(db_path)) + s.setValue("db/key", "test-key") + s.setValue("ui/idle_minutes", 0) + s.setValue("ui/theme", "light") + s.setValue("ui/tags", True) + s.setValue("ui/time_log", False) # Disable time log + + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + w = MainWindow(themes=themes) + qtbot.addWidget(w) + w.show() + + # Time log widget should be hidden + assert w.time_log.isHidden() + + +def test_export_csv_format(qtbot, app, tmp_path, monkeypatch): + """Test exporting to CSV format - covers export path lines""" + db_path = tmp_path / "notebook.db" + s = get_settings() + s.setValue("db/default_db", str(db_path)) + s.setValue("db/key", "test-key") + s.setValue("ui/idle_minutes", 0) + s.setValue("ui/theme", "light") + + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + w = MainWindow(themes=themes) + qtbot.addWidget(w) + w.show() + + # Add some data + w.db.save_new_version("2024-01-01", "Test content", "test") + + # Mock file dialog to return CSV + dest = tmp_path / "export_test.csv" + monkeypatch.setattr( + mwmod.QFileDialog, + "getSaveFileName", + staticmethod(lambda *a, **k: (str(dest), "CSV (*.csv)")), + ) + + # Mock QMessageBox to auto-accept + monkeypatch.setattr( + mwmod.QMessageBox, "exec", lambda self: mwmod.QMessageBox.Yes, raising=False + ) + monkeypatch.setattr( + mwmod.QMessageBox, "information", staticmethod(lambda *a, **k: 0), raising=False + ) + + w._export() + assert dest.exists() + + +def test_settings_dialog_with_locale_change(qtbot, app, tmp_path, monkeypatch): + """Test opening settings dialog and changing locale - covers settings dialog paths""" + db_path = tmp_path / "notebook.db" + s = get_settings() + s.setValue("db/default_db", str(db_path)) + s.setValue("db/key", "test-key") + s.setValue("ui/idle_minutes", 0) + s.setValue("ui/theme", "light") + + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + w = MainWindow(themes=themes) + qtbot.addWidget(w) + w.show() + + # Mock the settings dialog to auto-accept + from bouquin.settings_dialog import SettingsDialog + + SettingsDialog.exec + + def fake_exec(self): + # Change locale before accepting + idx = self.locale_combobox.findData("fr") + if idx >= 0: + self.locale_combobox.setCurrentIndex(idx) + return mwmod.QDialog.Accepted + + monkeypatch.setattr(SettingsDialog, "exec", fake_exec) + + w._open_settings() + qtbot.wait(50) + + +def test_statistics_dialog_open(qtbot, app, tmp_path, monkeypatch): + """Test opening statistics dialog - covers statistics dialog paths""" + db_path = tmp_path / "notebook.db" + s = get_settings() + s.setValue("db/default_db", str(db_path)) + s.setValue("db/key", "test-key") + s.setValue("ui/idle_minutes", 0) + s.setValue("ui/theme", "light") + + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + w = MainWindow(themes=themes) + qtbot.addWidget(w) + w.show() + + # Add some data + w.db.save_new_version("2024-01-01", "Test content", "test") + + from bouquin.statistics_dialog import StatisticsDialog + + StatisticsDialog.exec + + def fake_exec(self): + # Just accept immediately + return mwmod.QDialog.Accepted + + monkeypatch.setattr(StatisticsDialog, "exec", fake_exec) + + w._open_statistics() + qtbot.wait(50) + + +def test_bug_report_dialog_open(qtbot, app, tmp_path, monkeypatch): + """Test opening bug report dialog""" + db_path = tmp_path / "notebook.db" + s = get_settings() + s.setValue("db/default_db", str(db_path)) + s.setValue("db/key", "test-key") + s.setValue("ui/idle_minutes", 0) + s.setValue("ui/theme", "light") + + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + w = MainWindow(themes=themes) + qtbot.addWidget(w) + w.show() + + from bouquin.bug_report_dialog import BugReportDialog + + BugReportDialog.exec + + def fake_exec(self): + return mwmod.QDialog.Rejected + + monkeypatch.setattr(BugReportDialog, "exec", fake_exec) + + w._open_bugs() + qtbot.wait(50) + + +def test_history_dialog_open_and_restore(qtbot, app, tmp_path, monkeypatch): + """Test opening history dialog and restoring a version""" + db_path = tmp_path / "notebook.db" + s = get_settings() + s.setValue("db/default_db", str(db_path)) + s.setValue("db/key", "test-key") + s.setValue("ui/idle_minutes", 0) + s.setValue("ui/theme", "light") + + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + w = MainWindow(themes=themes) + qtbot.addWidget(w) + w.show() + + # Add some data + date_str = QDate.currentDate().toString("yyyy-MM-dd") + w.db.save_new_version(date_str, "Version 1", "v1") + w.db.save_new_version(date_str, "Version 2", "v2") + + from bouquin.history_dialog import HistoryDialog + + def fake_exec(self): + # Simulate selecting first version and accepting + if self.list.count() > 0: + self.list.setCurrentRow(0) + self._revert() + return mwmod.QDialog.Accepted + + monkeypatch.setattr(HistoryDialog, "exec", fake_exec) + + w._open_history() + qtbot.wait(50) + + +def test_goto_today_button(qtbot, app, tmp_path): + """Test going to today's date""" + db_path = tmp_path / "notebook.db" + s = get_settings() + s.setValue("db/default_db", str(db_path)) + s.setValue("db/key", "test-key") + s.setValue("ui/idle_minutes", 0) + s.setValue("ui/theme", "light") + + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + w = MainWindow(themes=themes) + qtbot.addWidget(w) + w.show() + + # Move to a different date + past_date = QDate.currentDate().addDays(-30) + w.calendar.setSelectedDate(past_date) + + # Go back to today + w._adjust_today() + qtbot.wait(50) + + assert w.calendar.selectedDate() == QDate.currentDate() + + +def test_adjust_font_size(qtbot, app, tmp_path): + """Test adjusting font size""" + db_path = tmp_path / "notebook.db" + s = get_settings() + s.setValue("db/default_db", str(db_path)) + s.setValue("db/key", "test-key") + s.setValue("ui/idle_minutes", 0) + s.setValue("ui/theme", "light") + s.setValue("ui/font_size", 12) + + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + w = MainWindow(themes=themes) + qtbot.addWidget(w) + w.show() + + initial_size = w.editor.font().pointSize() + + # Increase font size + w._on_font_larger_requested() + qtbot.wait(50) + assert w.editor.font().pointSize() > initial_size + + # Decrease font size + w._on_font_smaller_requested() + qtbot.wait(50) + + +def test_calendar_date_selection(qtbot, app, tmp_path): + """Test selecting a date from calendar""" + db_path = tmp_path / "notebook.db" + s = get_settings() + s.setValue("db/default_db", str(db_path)) + s.setValue("db/key", "test-key") + s.setValue("ui/idle_minutes", 0) + s.setValue("ui/theme", "light") + + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + w = MainWindow(themes=themes) + qtbot.addWidget(w) + w.show() + + # Select a specific date + test_date = QDate(2024, 6, 15) + w.calendar.setSelectedDate(test_date) + qtbot.wait(50) + + # The window should load that date + assert test_date.toString("yyyy-MM-dd") in str(w._current_date_iso()) diff --git a/tests/test_markdown_editor.py b/tests/test_markdown_editor.py index 9294773..8097a2e 100644 --- a/tests/test_markdown_editor.py +++ b/tests/test_markdown_editor.py @@ -1,3 +1,4 @@ +import base64 import pytest from PySide6.QtCore import Qt, QPoint @@ -1808,3 +1809,453 @@ def test_insert_alarm_marker_on_checkbox_line_does_not_merge_lines(editor, qtbot # Second line has the alarm marker assert "Foobar" in lines[1] assert "⏰ 16:54" in lines[1] + + +def test_render_images_with_corrupted_data(qtbot, app): + """Test rendering images with corrupted data that creates null QImage""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + # Create some binary data that will decode but not form a valid image + corrupted_data = base64.b64encode(b"not an image file").decode("utf-8") + markdown = f"![corrupted](data:image/png;base64,{corrupted_data})" + + editor.from_markdown(markdown) + qtbot.wait(50) + + # Should still work without crashing + text = editor.to_markdown() + assert len(text) >= 0 + + +def test_insert_alarm_marker(qtbot, app): + """Test inserting alarm markers""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + # Insert alarm marker + editor.insert_alarm_marker("14:30") + qtbot.wait(50) + + content = editor.to_markdown() + assert "14:30" in content or "⏰" in content + + +def test_editor_with_tables(qtbot, app): + """Test editor with markdown tables""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + table_markdown = """ +| Header 1 | Header 2 | +|----------|----------| +| Cell 1 | Cell 2 | +| Cell 3 | Cell 4 | +""" + editor.from_markdown(table_markdown) + qtbot.wait(50) + + result = editor.to_markdown() + assert "Header 1" in result or "|" in result + + +def test_editor_with_code_blocks(qtbot, app): + """Test editor with code blocks""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + code_markdown = """ +Some text + +```python +def hello(): + print("world") +``` + +More text +""" + editor.from_markdown(code_markdown) + qtbot.wait(50) + + result = editor.to_markdown() + assert "def hello" in result or "python" in result + + +def test_editor_undo_redo(qtbot, app): + """Test undo/redo functionality""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + # Type some text + editor.from_markdown("Initial text") + qtbot.wait(50) + + # Add more text + editor.insertPlainText(" additional") + qtbot.wait(50) + + # Undo + editor.undo() + qtbot.wait(50) + + # Redo + editor.redo() + qtbot.wait(50) + + assert len(editor.to_markdown()) > 0 + + +def test_editor_cut_copy_paste(qtbot, app): + """Test cut/copy/paste operations""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + editor.from_markdown("Test content for copy") + qtbot.wait(50) + + # Select all + editor.selectAll() + + # Copy + editor.copy() + qtbot.wait(50) + + # Move to end and paste + cursor = editor.textCursor() + cursor.movePosition(QTextCursor.End) + editor.setTextCursor(cursor) + + editor.paste() + qtbot.wait(50) + + # Should have content twice (or clipboard might be empty in test env) + assert len(editor.to_markdown()) > 0 + + +def test_editor_with_blockquotes(qtbot, app): + """Test editor with blockquotes""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + quote_markdown = """ +Normal text + +> This is a quote +> With multiple lines + +More normal text +""" + editor.from_markdown(quote_markdown) + qtbot.wait(50) + + result = editor.to_markdown() + assert ">" in result or "quote" in result + + +def test_editor_with_horizontal_rules(qtbot, app): + """Test editor with horizontal rules""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + hr_markdown = """ +Section 1 + +--- + +Section 2 +""" + editor.from_markdown(hr_markdown) + qtbot.wait(50) + + result = editor.to_markdown() + assert "Section" in result + + +def test_editor_with_mixed_content(qtbot, app): + """Test editor with mixed markdown content""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + mixed_markdown = """ +# Heading + +This is **bold** and *italic* text. + +- [ ] Todo item +- [x] Completed item + +```python +code() +``` + +[Link](https://example.com) + +> Quote + +| Table | Header | +|-------|--------| +| A | B | +""" + editor.from_markdown(mixed_markdown) + qtbot.wait(50) + + result = editor.to_markdown() + # Should contain various markdown elements + assert len(result) > 50 + + +def test_editor_insert_text_at_cursor(qtbot, app): + """Test inserting text at cursor position""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + editor.from_markdown("Start Middle End") + qtbot.wait(50) + + # Move cursor to middle + cursor = editor.textCursor() + cursor.setPosition(6) + editor.setTextCursor(cursor) + + # Insert text + editor.insertPlainText("INSERTED ") + qtbot.wait(50) + + result = editor.to_markdown() + assert "INSERTED" in result + + +def test_editor_delete_operations(qtbot, app): + """Test delete operations""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + editor = MarkdownEditor(theme_manager=themes) + qtbot.addWidget(editor) + editor.show() + + editor.from_markdown("Text to delete") + qtbot.wait(50) + + # Select some text and delete + cursor = editor.textCursor() + cursor.setPosition(0) + cursor.setPosition(4, QTextCursor.KeepAnchor) + editor.setTextCursor(cursor) + + cursor.removeSelectedText() + qtbot.wait(50) + + result = editor.to_markdown() + assert "Text" not in result or len(result) < 15 + + +def test_markdown_highlighter_dark_theme(qtbot, app): + """Test markdown highlighter with dark theme - covers lines 74-75""" + # Create theme manager with dark theme + themes = ThemeManager(app, ThemeConfig(theme=Theme.DARK)) + + # Create a text document + doc = QTextDocument() + + # Create highlighter with dark theme + highlighter = MarkdownHighlighter(doc, themes) + + # Set some markdown text + doc.setPlainText("# Heading\n\nSome **bold** text\n\n```python\ncode\n```") + + # The highlighter should work with dark theme + assert highlighter is not None + assert highlighter.code_block_format is not None + + +def test_markdown_highlighter_light_theme(qtbot, app): + """Test markdown highlighter with light theme""" + # Create theme manager with light theme + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + + # Create a text document + doc = QTextDocument() + + # Create highlighter with light theme + highlighter = MarkdownHighlighter(doc, themes) + + # Set some markdown text + doc.setPlainText("# Heading\n\nSome **bold** text") + + # The highlighter should work with light theme + assert highlighter is not None + assert highlighter.code_block_format is not None + + +def test_markdown_highlighter_system_dark_theme(qtbot, app, monkeypatch): + """Test markdown highlighter with system dark theme""" + # Create theme manager with system theme + themes = ThemeManager(app, ThemeConfig(theme=Theme.SYSTEM)) + + # Mock the system to be dark + monkeypatch.setattr(themes, "_is_system_dark", True) + + # Create a text document + doc = QTextDocument() + + # Create highlighter + highlighter = MarkdownHighlighter(doc, themes) + + # Set some markdown text + doc.setPlainText("# Dark Theme Heading\n\n**Bold text**") + + # The highlighter should use dark theme colors + assert highlighter is not None + + +def test_markdown_highlighter_with_headings(qtbot, app): + """Test highlighting various heading levels""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + doc = QTextDocument() + highlighter = MarkdownHighlighter(doc, themes) + + markdown = """ +# H1 Heading +## H2 Heading +### H3 Heading +#### H4 Heading +##### H5 Heading +###### H6 Heading +""" + doc.setPlainText(markdown) + + # Should highlight all headings + assert highlighter.h1_format is not None + assert highlighter.h2_format is not None + + +def test_markdown_highlighter_with_emphasis(qtbot, app): + """Test highlighting bold and italic""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + doc = QTextDocument() + highlighter = MarkdownHighlighter(doc, themes) + + markdown = """ +**Bold text** +*Italic text* +***Bold and italic*** +__Also bold__ +_Also italic_ +""" + doc.setPlainText(markdown) + + # Should have emphasis formats + assert highlighter is not None + + +def test_markdown_highlighter_with_code(qtbot, app): + """Test highlighting inline code and code blocks""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + doc = QTextDocument() + highlighter = MarkdownHighlighter(doc, themes) + + markdown = """ +Inline `code` here. + +```python +def hello(): + print("world") +``` + +More text. +""" + doc.setPlainText(markdown) + + # Should highlight code + assert highlighter.code_block_format is not None + + +def test_markdown_highlighter_with_links(qtbot, app): + """Test highlighting links""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + doc = QTextDocument() + highlighter = MarkdownHighlighter(doc, themes) + + markdown = """ +[Link text](https://example.com) + +""" + doc.setPlainText(markdown) + + # Should have link format + assert highlighter is not None + + +def test_markdown_highlighter_with_lists(qtbot, app): + """Test highlighting lists""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + doc = QTextDocument() + highlighter = MarkdownHighlighter(doc, themes) + + markdown = """ +- Unordered item 1 +- Unordered item 2 + +1. Ordered item 1 +2. Ordered item 2 + +- [ ] Unchecked task +- [x] Checked task +""" + doc.setPlainText(markdown) + + # Should highlight lists + assert highlighter is not None + + +def test_markdown_highlighter_with_blockquotes(qtbot, app): + """Test highlighting blockquotes""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + doc = QTextDocument() + highlighter = MarkdownHighlighter(doc, themes) + + markdown = """ +> This is a quote +> With multiple lines +""" + doc.setPlainText(markdown) + + # Should highlight quotes + assert highlighter is not None + + +def test_markdown_highlighter_theme_change(qtbot, app): + """Test changing theme after creation""" + themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) + doc = QTextDocument() + highlighter = MarkdownHighlighter(doc, themes) + + markdown = "# Heading\n\n**Bold**" + doc.setPlainText(markdown) + + # Change to dark theme + themes.apply(Theme.DARK) + qtbot.wait(50) + + # Highlighter should update + # We can't directly test the visual change, but verify it doesn't crash + assert highlighter is not None diff --git a/tests/test_statistics_dialog.py b/tests/test_statistics_dialog.py index 7359250..4fc213f 100644 --- a/tests/test_statistics_dialog.py +++ b/tests/test_statistics_dialog.py @@ -1,11 +1,12 @@ import datetime as _dt +from datetime import datetime, timedelta, date from bouquin import strings -from datetime import date from PySide6.QtCore import Qt, QPoint from PySide6.QtWidgets import QLabel from PySide6.QtTest import QTest +from PySide6.QtCore import QDate from bouquin.statistics_dialog import DateHeatmap, StatisticsDialog @@ -417,3 +418,100 @@ def test_statistics_dialog_gather_stats_exception_handling( # Should handle error gracefully assert dialog.isVisible() + + +def test_statistics_dialog_with_sparse_data(qtbot, tmp_db_cfg, fresh_db): + """Test statistics dialog with sparse data""" + # Add some entries on non-consecutive days + dates = ["2024-01-01", "2024-01-05", "2024-01-10", "2024-01-20"] + for _date in dates: + content = "Word " * 100 # 100 words + fresh_db.save_new_version(_date, content, "note") + + dialog = StatisticsDialog(fresh_db) + qtbot.addWidget(dialog) + + # Should create without crashing + assert dialog is not None + + +def test_statistics_dialog_with_empty_data(qtbot, tmp_db_cfg, fresh_db): + """Test statistics dialog with no data""" + dialog = StatisticsDialog(fresh_db) + qtbot.addWidget(dialog) + + # Should handle empty data gracefully + assert dialog is not None + + +def test_statistics_dialog_date_range_selection(qtbot, tmp_db_cfg, fresh_db): + """Test changing metric in statistics dialog""" + # Add some test data + for i in range(10): + date = QDate.currentDate().addDays(-i).toString("yyyy-MM-dd") + fresh_db.save_new_version(date, f"Content for day {i}", "note") + + dialog = StatisticsDialog(fresh_db) + qtbot.addWidget(dialog) + + # Change metric to revisions + idx = dialog.metric_combo.findData("revisions") + if idx >= 0: + dialog.metric_combo.setCurrentIndex(idx) + qtbot.wait(50) + + # Change back to words + idx = dialog.metric_combo.findData("words") + if idx >= 0: + dialog.metric_combo.setCurrentIndex(idx) + qtbot.wait(50) + + +def test_heatmap_with_varying_word_counts(qtbot): + """Test heatmap color scaling with varying word counts""" + today = datetime.now().date() + start = today - timedelta(days=30) + + entries = {} + # Create entries with varying word counts + for i in range(31): + date = start + timedelta(days=i) + entries[date] = i * 50 # Increasing word counts + + heatmap = DateHeatmap() + heatmap.set_data(entries) + qtbot.addWidget(heatmap) + heatmap.show() + + # Should paint without errors + assert heatmap.isVisible() + + +def test_heatmap_single_day(qtbot): + """Test heatmap with single day of data""" + today = datetime.now().date() + entries = {today: 500} + + heatmap = DateHeatmap() + heatmap.set_data(entries) + qtbot.addWidget(heatmap) + heatmap.show() + + assert heatmap.isVisible() + + +def test_statistics_dialog_metric_changes(qtbot, tmp_db_cfg, fresh_db): + """Test various metric selections""" + # Add data spanning multiple months + base_date = QDate.currentDate().addDays(-90) + for i in range(90): + date = base_date.addDays(i).toString("yyyy-MM-dd") + fresh_db.save_new_version(date, f"Day {i} content with many words", "note") + + dialog = StatisticsDialog(fresh_db) + qtbot.addWidget(dialog) + + # Test each metric option + for i in range(dialog.metric_combo.count()): + dialog.metric_combo.setCurrentIndex(i) + qtbot.wait(50) diff --git a/tests/test_tags.py b/tests/test_tags.py index 4374458..8564c6b 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -1,7 +1,14 @@ import pytest -from PySide6.QtCore import Qt, QPoint, QEvent + +from PySide6.QtCore import Qt, QPoint, QEvent, QDate from PySide6.QtGui import QMouseEvent, QColor -from PySide6.QtWidgets import QApplication, QMessageBox, QInputDialog, QColorDialog +from PySide6.QtWidgets import ( + QApplication, + QMessageBox, + QInputDialog, + QColorDialog, + QDialog, +) from bouquin.db import DBManager from bouquin.strings import load_strings from bouquin.tags_widget import PageTagsWidget, TagChip @@ -2157,3 +2164,156 @@ def test_page_tags_widget_updates_on_tag_change(qtbot, fresh_db): qtbot.wait(100) assert widget.chip_layout.count() == 2 + + +def test_tags_widget_open_manager_and_accept(qtbot, tmp_db_cfg, fresh_db, monkeypatch): + """Test opening tag manager dialog and accepting - covers lines 248-256""" + tags_widget = PageTagsWidget(fresh_db) + qtbot.addWidget(tags_widget) + + # Set a current date + date = QDate.currentDate().toString("yyyy-MM-dd") + tags_widget.set_current_date(date) + + # Add some tags first + fresh_db.add_tag("Test Tag", date) + tags_widget._reload_tags() + + # Mock the tag browser dialog + from bouquin.tag_browser import TagBrowserDialog + + dialog_executed = [] + + def fake_exec(self): + dialog_executed.append(True) + # Simulate the dialog being accepted + return QDialog.Accepted + + monkeypatch.setattr(TagBrowserDialog, "exec", fake_exec) + + # Open the manager + tags_widget._open_manager() + qtbot.wait(50) + + # Dialog should have been executed + assert len(dialog_executed) > 0 + + +def test_tags_widget_open_manager_and_reject(qtbot, tmp_db_cfg, fresh_db, monkeypatch): + """Test opening tag manager dialog and rejecting""" + tags_widget = PageTagsWidget(fresh_db) + qtbot.addWidget(tags_widget) + + # Set a current date + date = QDate.currentDate().toString("yyyy-MM-dd") + tags_widget.set_current_date(date) + + # Mock the tag browser dialog + from bouquin.tag_browser import TagBrowserDialog + + dialog_executed = [] + + def fake_exec(self): + dialog_executed.append(True) + # Simulate the dialog being rejected + return QDialog.Rejected + + monkeypatch.setattr(TagBrowserDialog, "exec", fake_exec) + + # Open the manager + tags_widget._open_manager() + qtbot.wait(50) + + # Dialog should have been executed + assert len(dialog_executed) > 0 + + +def test_tags_widget_open_manager_without_current_date( + qtbot, tmp_db_cfg, fresh_db, monkeypatch +): + """Test opening tag manager when no current date is set""" + tags_widget = PageTagsWidget(fresh_db) + qtbot.addWidget(tags_widget) + + # Don't set a current date + tags_widget._current_date = None + + # Mock the tag browser dialog + from bouquin.tag_browser import TagBrowserDialog + + dialog_executed = [] + + def fake_exec(self): + dialog_executed.append(True) + return QDialog.Accepted + + monkeypatch.setattr(TagBrowserDialog, "exec", fake_exec) + + # Open the manager + tags_widget._open_manager() + qtbot.wait(50) + + # Dialog should still execute + assert len(dialog_executed) > 0 + + +def test_tags_widget_manager_with_date_click_signal( + qtbot, tmp_db_cfg, fresh_db, monkeypatch +): + """Test tag manager emitting openDateRequested signal""" + tags_widget = PageTagsWidget(fresh_db) + qtbot.addWidget(tags_widget) + + date = QDate.currentDate().toString("yyyy-MM-dd") + tags_widget.set_current_date(date) + + activated_tags = [] + + def capture_tag(tag): + activated_tags.append(tag) + + tags_widget.tagActivated.connect(capture_tag) + + # Mock the tag browser dialog + from bouquin.tag_browser import TagBrowserDialog + + def fake_exec(self): + # Simulate clicking a date in the browser + self.openDateRequested.emit("2024-01-01") + return QDialog.Accepted + + monkeypatch.setattr(TagBrowserDialog, "exec", fake_exec) + + # Open the manager + tags_widget._open_manager() + qtbot.wait(50) + + # Should have captured the activated tag/date + assert len(activated_tags) > 0 + assert "2024-01-01" in activated_tags + + +def test_tags_widget_chip_click(qtbot, tmp_db_cfg, fresh_db): + """Test clicking on a tag chip""" + tags_widget = PageTagsWidget(fresh_db) + qtbot.addWidget(tags_widget) + + date = QDate.currentDate().toString("yyyy-MM-dd") + tags_widget.set_current_date(date) + + # Add a tag + fresh_db.add_tag("ClickMe", date) + tags_widget._reload_tags() + + activated_tags = [] + + def capture_tag(tag): + activated_tags.append(tag) + + tags_widget.tagActivated.connect(capture_tag) + + # Simulate chip click + tags_widget._on_chip_clicked("ClickMe") + qtbot.wait(50) + + assert "ClickMe" in activated_tags