More tests
All checks were successful
CI / test (push) Successful in 4m40s
Lint / test (push) Successful in 31s
Trivy / test (push) Successful in 23s

This commit is contained in:
Miguel Jacq 2025-11-21 14:30:38 +11:00
parent e8db5bcf7d
commit ca3c839c7d
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
5 changed files with 1184 additions and 3 deletions

View file

@ -1,5 +1,8 @@
from bouquin.key_prompt import KeyPrompt from bouquin.key_prompt import KeyPrompt
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QFileDialog, QLineEdit
def test_key_prompt_roundtrip(qtbot): def test_key_prompt_roundtrip(qtbot):
kp = KeyPrompt() kp = KeyPrompt()
@ -7,3 +10,196 @@ def test_key_prompt_roundtrip(qtbot):
kp.show() kp.show()
kp.key_entry.setText("swordfish") kp.key_entry.setText("swordfish")
assert kp.key() == "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

View file

@ -1812,3 +1812,279 @@ def test_main_window_update_tag_views_no_tags_widget(
window._update_tag_views_for_date("2024-01-15") window._update_tag_views_for_date("2024-01-15")
assert True 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())

View file

@ -1,3 +1,4 @@
import base64
import pytest import pytest
from PySide6.QtCore import Qt, QPoint 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 # Second line has the alarm marker
assert "Foobar" in lines[1] assert "Foobar" in lines[1]
assert "⏰ 16:54" 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)
<https://auto-link.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

View file

@ -1,11 +1,12 @@
import datetime as _dt import datetime as _dt
from datetime import datetime, timedelta, date
from bouquin import strings from bouquin import strings
from datetime import date
from PySide6.QtCore import Qt, QPoint from PySide6.QtCore import Qt, QPoint
from PySide6.QtWidgets import QLabel from PySide6.QtWidgets import QLabel
from PySide6.QtTest import QTest from PySide6.QtTest import QTest
from PySide6.QtCore import QDate
from bouquin.statistics_dialog import DateHeatmap, StatisticsDialog from bouquin.statistics_dialog import DateHeatmap, StatisticsDialog
@ -417,3 +418,100 @@ def test_statistics_dialog_gather_stats_exception_handling(
# Should handle error gracefully # Should handle error gracefully
assert dialog.isVisible() 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)

View file

@ -1,7 +1,14 @@
import pytest 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.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.db import DBManager
from bouquin.strings import load_strings from bouquin.strings import load_strings
from bouquin.tags_widget import PageTagsWidget, TagChip 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) qtbot.wait(100)
assert widget.chip_layout.count() == 2 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