Fix tests, add vulture_ignorelist.py, fix markdown_editor highlighter bug
This commit is contained in:
parent
f6e10dccac
commit
02a60ca656
14 changed files with 2277 additions and 61 deletions
|
|
@ -15,7 +15,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
black pyflakes3
|
black pyflakes3 vulture
|
||||||
|
|
||||||
- name: Run linters
|
- name: Run linters
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -23,3 +23,4 @@ jobs:
|
||||||
black --diff --check tests/*
|
black --diff --check tests/*
|
||||||
pyflakes3 bouquin/*
|
pyflakes3 bouquin/*
|
||||||
pyflakes3 tests/*
|
pyflakes3 tests/*
|
||||||
|
vulture
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,10 @@ class MarkdownEditor(QTextEdit):
|
||||||
|
|
||||||
def setDocument(self, doc):
|
def setDocument(self, doc):
|
||||||
super().setDocument(doc)
|
super().setDocument(doc)
|
||||||
# reattach the highlighter to the new document
|
# Recreate the highlighter for the new document
|
||||||
if hasattr(self, "highlighter") and self.highlighter:
|
# (the old one gets deleted with the old document)
|
||||||
self.highlighter.setDocument(self.document())
|
if hasattr(self, "highlighter") and hasattr(self, "theme_manager"):
|
||||||
|
self.highlighter = MarkdownHighlighter(self.document(), self.theme_manager)
|
||||||
self._apply_line_spacing()
|
self._apply_line_spacing()
|
||||||
self._apply_code_block_spacing()
|
self._apply_code_block_spacing()
|
||||||
QTimer.singleShot(0, self._update_code_block_row_backgrounds)
|
QTimer.singleShot(0, self._update_code_block_row_backgrounds)
|
||||||
|
|
@ -97,7 +98,7 @@ class MarkdownEditor(QTextEdit):
|
||||||
line = block.text()
|
line = block.text()
|
||||||
pos_in_block = c.position() - block.position()
|
pos_in_block = c.position() - block.position()
|
||||||
|
|
||||||
# Transform markldown checkboxes and 'TODO' to unicode checkboxes
|
# Transform markdown checkboxes and 'TODO' to unicode checkboxes
|
||||||
def transform_line(s: str) -> str:
|
def transform_line(s: str) -> str:
|
||||||
s = s.replace(
|
s = s.replace(
|
||||||
f"- {self._CHECK_CHECKED_STORAGE} ",
|
f"- {self._CHECK_CHECKED_STORAGE} ",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ pyproject-appimage = "^4.2"
|
||||||
script = "bouquin"
|
script = "bouquin"
|
||||||
output = "Bouquin.AppImage"
|
output = "Bouquin.AppImage"
|
||||||
|
|
||||||
|
[tool.vulture]
|
||||||
|
paths = ["bouquin", "vulture_ignorelist.py"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ def editor(app, qtbot):
|
||||||
return ed
|
return ed
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_findbar_basic_navigation(qtbot, editor):
|
def test_findbar_basic_navigation(qtbot, editor):
|
||||||
editor.from_markdown("alpha\nbeta\nalpha\nGamma\n")
|
editor.from_markdown("alpha\nbeta\nalpha\nGamma\n")
|
||||||
editor.moveCursor(QTextCursor.Start)
|
editor.moveCursor(QTextCursor.Start)
|
||||||
|
|
@ -113,7 +112,6 @@ def test_update_highlight_clear_when_empty(qtbot, editor):
|
||||||
assert not editor.extraSelections()
|
assert not editor.extraSelections()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_maybe_hide_and_wrap_prev(qtbot, editor):
|
def test_maybe_hide_and_wrap_prev(qtbot, editor):
|
||||||
editor.setPlainText("a a a")
|
editor.setPlainText("a a a")
|
||||||
fb = FindBar(editor=editor, shortcut_parent=editor)
|
fb = FindBar(editor=editor, shortcut_parent=editor)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import pytest
|
|
||||||
from PySide6.QtCore import QEvent
|
from PySide6.QtCore import QEvent
|
||||||
from PySide6.QtWidgets import QWidget
|
from PySide6.QtWidgets import QWidget
|
||||||
from bouquin.lock_overlay import LockOverlay
|
from bouquin.lock_overlay import LockOverlay
|
||||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_lock_overlay_reacts_to_theme(app, qtbot):
|
def test_lock_overlay_reacts_to_theme(app, qtbot):
|
||||||
host = QWidget()
|
host = QWidget()
|
||||||
qtbot.addWidget(host)
|
qtbot.addWidget(host)
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,12 @@ from bouquin.settings import get_settings
|
||||||
from bouquin.key_prompt import KeyPrompt
|
from bouquin.key_prompt import KeyPrompt
|
||||||
from bouquin.db import DBConfig, DBManager
|
from bouquin.db import DBConfig, DBManager
|
||||||
from PySide6.QtCore import QEvent, QDate, QTimer, Qt, QPoint, QRect
|
from PySide6.QtCore import QEvent, QDate, QTimer, Qt, QPoint, QRect
|
||||||
from PySide6.QtWidgets import QTableView, QApplication, QWidget, QMessageBox
|
from PySide6.QtWidgets import QTableView, QApplication, QWidget, QMessageBox, QDialog
|
||||||
from PySide6.QtGui import QMouseEvent, QKeyEvent, QTextCursor, QCloseEvent
|
from PySide6.QtGui import QMouseEvent, QKeyEvent, QTextCursor, QCloseEvent
|
||||||
|
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_main_window_loads_and_saves(qtbot, app, tmp_db_cfg, fresh_db):
|
def test_main_window_loads_and_saves(qtbot, app, tmp_db_cfg, fresh_db):
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
s.setValue("db/path", str(tmp_db_cfg.path))
|
s.setValue("db/path", str(tmp_db_cfg.path))
|
||||||
|
|
@ -79,7 +80,6 @@ def test_load_yesterday_todos_moves_items(qtbot, app, tmp_db_cfg, fresh_db):
|
||||||
assert "carry me" not in y_txt or "- [ ]" not in y_txt
|
assert "carry me" not in y_txt or "- [ ]" not in y_txt
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_open_docs_and_bugs_warning(qtbot, app, fresh_db, tmp_path, monkeypatch):
|
def test_open_docs_and_bugs_warning(qtbot, app, fresh_db, tmp_path, monkeypatch):
|
||||||
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
w = MainWindow(themes=themes)
|
w = MainWindow(themes=themes)
|
||||||
|
|
@ -113,7 +113,6 @@ def test_open_docs_and_bugs_warning(qtbot, app, fresh_db, tmp_path, monkeypatch)
|
||||||
assert called["docs"] and called["bugs"]
|
assert called["docs"] and called["bugs"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_export_success_and_error(qtbot, app, fresh_db, tmp_path, monkeypatch):
|
def test_export_success_and_error(qtbot, app, fresh_db, tmp_path, monkeypatch):
|
||||||
# Seed some content
|
# Seed some content
|
||||||
fresh_db.save_new_version("2001-01-01", "alpha", "n1")
|
fresh_db.save_new_version("2001-01-01", "alpha", "n1")
|
||||||
|
|
@ -190,7 +189,6 @@ def test_export_success_and_error(qtbot, app, fresh_db, tmp_path, monkeypatch):
|
||||||
assert errs["hit"]
|
assert errs["hit"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_backup_path(qtbot, app, fresh_db, tmp_path, monkeypatch):
|
def test_backup_path(qtbot, app, fresh_db, tmp_path, monkeypatch):
|
||||||
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
|
||||||
|
|
@ -248,7 +246,6 @@ def test_backup_path(qtbot, app, fresh_db, tmp_path, monkeypatch):
|
||||||
assert str(dest.with_suffix(".db")) in hit["text"]
|
assert str(dest.with_suffix(".db")) in hit["text"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_close_tab_edges_and_autosave(qtbot, app, fresh_db, monkeypatch):
|
def test_close_tab_edges_and_autosave(qtbot, app, fresh_db, monkeypatch):
|
||||||
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
from bouquin.settings import get_settings
|
from bouquin.settings import get_settings
|
||||||
|
|
@ -283,7 +280,6 @@ def test_close_tab_edges_and_autosave(qtbot, app, fresh_db, monkeypatch):
|
||||||
monkeypatch.delattr(w, "_save_editor_content", raising=False)
|
monkeypatch.delattr(w, "_save_editor_content", raising=False)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_restore_window_position_variants(qtbot, app, fresh_db, monkeypatch):
|
def test_restore_window_position_variants(qtbot, app, fresh_db, monkeypatch):
|
||||||
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
w = MainWindow(themes=themes)
|
w = MainWindow(themes=themes)
|
||||||
|
|
@ -329,7 +325,6 @@ def test_restore_window_position_variants(qtbot, app, fresh_db, monkeypatch):
|
||||||
assert called["max"]
|
assert called["max"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_calendar_pos_mapping_and_context_menu(qtbot, app, fresh_db, monkeypatch):
|
def test_calendar_pos_mapping_and_context_menu(qtbot, app, fresh_db, monkeypatch):
|
||||||
# Seed DB so refresh marks does something
|
# Seed DB so refresh marks does something
|
||||||
fresh_db.save_new_version("2021-08-15", "note", "")
|
fresh_db.save_new_version("2021-08-15", "note", "")
|
||||||
|
|
@ -402,7 +397,6 @@ def test_calendar_pos_mapping_and_context_menu(qtbot, app, fresh_db, monkeypatch
|
||||||
w._show_calendar_context_menu(cal_pos)
|
w._show_calendar_context_menu(cal_pos)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_event_filter_keypress_starts_idle_timer(qtbot, app):
|
def test_event_filter_keypress_starts_idle_timer(qtbot, app):
|
||||||
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
w = MainWindow(themes=themes)
|
w = MainWindow(themes=themes)
|
||||||
|
|
@ -1027,7 +1021,6 @@ def test_rect_on_any_screen_false(qtbot, tmp_db_cfg, app, monkeypatch):
|
||||||
assert not w._rect_on_any_screen(far)
|
assert not w._rect_on_any_screen(far)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_reorder_tabs_with_undated_page_stays_last(qtbot, app, tmp_db_cfg, monkeypatch):
|
def test_reorder_tabs_with_undated_page_stays_last(qtbot, app, tmp_db_cfg, monkeypatch):
|
||||||
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
||||||
qtbot.addWidget(w)
|
qtbot.addWidget(w)
|
||||||
|
|
@ -1103,7 +1096,6 @@ def test_on_tab_changed_early_and_stop_guard(qtbot, app, tmp_db_cfg, monkeypatch
|
||||||
w._on_tab_changed(1) # should not raise
|
w._on_tab_changed(1) # should not raise
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_show_calendar_context_menu_no_action(qtbot, app, tmp_db_cfg, monkeypatch):
|
def test_show_calendar_context_menu_no_action(qtbot, app, tmp_db_cfg, monkeypatch):
|
||||||
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
||||||
qtbot.addWidget(w)
|
qtbot.addWidget(w)
|
||||||
|
|
@ -1124,7 +1116,6 @@ def test_show_calendar_context_menu_no_action(qtbot, app, tmp_db_cfg, monkeypatc
|
||||||
assert w.tab_widget.count() == before
|
assert w.tab_widget.count() == before
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_export_cancel_then_empty_filename(
|
def test_export_cancel_then_empty_filename(
|
||||||
qtbot, app, tmp_db_cfg, monkeypatch, tmp_path
|
qtbot, app, tmp_db_cfg, monkeypatch, tmp_path
|
||||||
):
|
):
|
||||||
|
|
@ -1187,7 +1178,6 @@ def test_export_cancel_then_empty_filename(
|
||||||
w._export() # returns early at filename check
|
w._export() # returns early at filename check
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_set_editor_markdown_preserve_view_preserves(
|
def test_set_editor_markdown_preserve_view_preserves(
|
||||||
qtbot, app, tmp_db_cfg, monkeypatch
|
qtbot, app, tmp_db_cfg, monkeypatch
|
||||||
):
|
):
|
||||||
|
|
@ -1212,7 +1202,6 @@ def test_set_editor_markdown_preserve_view_preserves(
|
||||||
assert w.editor.to_markdown().endswith("extra\n")
|
assert w.editor.to_markdown().endswith("extra\n")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_load_date_into_editor_with_extra_data_forces_save(
|
def test_load_date_into_editor_with_extra_data_forces_save(
|
||||||
qtbot, app, tmp_db_cfg, monkeypatch
|
qtbot, app, tmp_db_cfg, monkeypatch
|
||||||
):
|
):
|
||||||
|
|
@ -1230,7 +1219,6 @@ def test_load_date_into_editor_with_extra_data_forces_save(
|
||||||
assert called["iso"] == "2020-01-01" and called["explicit"] is True
|
assert called["iso"] == "2020-01-01" and called["explicit"] is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_reorder_tabs_moves_and_undated(qtbot, app, tmp_db_cfg, monkeypatch):
|
def test_reorder_tabs_moves_and_undated(qtbot, app, tmp_db_cfg, monkeypatch):
|
||||||
"""Covers moveTab for both dated and undated buckets."""
|
"""Covers moveTab for both dated and undated buckets."""
|
||||||
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
||||||
|
|
@ -1324,7 +1312,6 @@ def test_date_from_calendar_no_first_or_last(qtbot, app, tmp_db_cfg, monkeypatch
|
||||||
assert w._date_from_calendar_pos(QPoint(5, 5)) is None
|
assert w._date_from_calendar_pos(QPoint(5, 5)) is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_save_editor_content_returns_if_no_conn(qtbot, app, tmp_db_cfg, monkeypatch):
|
def test_save_editor_content_returns_if_no_conn(qtbot, app, tmp_db_cfg, monkeypatch):
|
||||||
"""Covers DB not connected branch."""
|
"""Covers DB not connected branch."""
|
||||||
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
||||||
|
|
@ -1337,7 +1324,6 @@ def test_save_editor_content_returns_if_no_conn(qtbot, app, tmp_db_cfg, monkeypa
|
||||||
w._save_editor_content(w.editor)
|
w._save_editor_content(w.editor)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_on_date_changed_stops_timer_and_saves_prev_when_dirty(
|
def test_on_date_changed_stops_timer_and_saves_prev_when_dirty(
|
||||||
qtbot, app, tmp_db_cfg, monkeypatch
|
qtbot, app, tmp_db_cfg, monkeypatch
|
||||||
):
|
):
|
||||||
|
|
@ -1370,7 +1356,6 @@ def test_on_date_changed_stops_timer_and_saves_prev_when_dirty(
|
||||||
assert saved["iso"] == "2024-01-01"
|
assert saved["iso"] == "2024-01-01"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_bind_toolbar_idempotent(qtbot, app, tmp_db_cfg, monkeypatch):
|
def test_bind_toolbar_idempotent(qtbot, app, tmp_db_cfg, monkeypatch):
|
||||||
"""Covers early return when toolbar is already bound."""
|
"""Covers early return when toolbar is already bound."""
|
||||||
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
||||||
|
|
@ -1380,7 +1365,6 @@ def test_bind_toolbar_idempotent(qtbot, app, tmp_db_cfg, monkeypatch):
|
||||||
w._bind_toolbar()
|
w._bind_toolbar()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_on_insert_image_no_selection_returns(qtbot, app, tmp_db_cfg, monkeypatch):
|
def test_on_insert_image_no_selection_returns(qtbot, app, tmp_db_cfg, monkeypatch):
|
||||||
"""Covers the early return when user selects no files."""
|
"""Covers the early return when user selects no files."""
|
||||||
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
w = _make_main_window(tmp_db_cfg, app, monkeypatch)
|
||||||
|
|
@ -1420,7 +1404,6 @@ def test_apply_idle_minutes_starts_when_unlocked(qtbot, app, tmp_db_cfg, monkeyp
|
||||||
assert hit["start"]
|
assert hit["start"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_close_event_handles_settings_failures(qtbot, app, tmp_db_cfg, monkeypatch):
|
def test_close_event_handles_settings_failures(qtbot, app, tmp_db_cfg, monkeypatch):
|
||||||
"""
|
"""
|
||||||
Covers exception swallowing around settings writes & ensures close proceeds
|
Covers exception swallowing around settings writes & ensures close proceeds
|
||||||
|
|
@ -1488,3 +1471,328 @@ def test_closeEvent_swallows_exceptions(qtbot, app, tmp_db_cfg, monkeypatch):
|
||||||
w.closeEvent(ev)
|
w.closeEvent(ev)
|
||||||
|
|
||||||
assert called["save"] and called["close"]
|
assert called["save"] and called["close"]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tag Save Handler Tests (lines 1050-1068)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_do_tag_save_with_editor(app, fresh_db, tmp_db_cfg, monkeypatch):
|
||||||
|
"""Test _do_tag_save when editor has current_date"""
|
||||||
|
# Skip the key prompt
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
# Set a date on the editor
|
||||||
|
date = QDate(2024, 1, 15)
|
||||||
|
window.editor.current_date = date
|
||||||
|
window.editor.from_markdown("Test content")
|
||||||
|
|
||||||
|
# Call _do_tag_save
|
||||||
|
window._do_tag_save()
|
||||||
|
|
||||||
|
# Should have saved
|
||||||
|
fresh_db.get_entry("2024-01-15")
|
||||||
|
# May or may not have content depending on timing, but should not crash
|
||||||
|
assert True
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_do_tag_save_no_editor(app, fresh_db, tmp_db_cfg, monkeypatch):
|
||||||
|
"""Test _do_tag_save when editor doesn't have current_date"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
# Remove current_date attribute
|
||||||
|
if hasattr(window.editor, "current_date"):
|
||||||
|
delattr(window.editor, "current_date")
|
||||||
|
|
||||||
|
# Call _do_tag_save - should handle gracefully
|
||||||
|
window._do_tag_save()
|
||||||
|
|
||||||
|
assert True
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_on_tag_added_triggers_deferred_save(
|
||||||
|
app, fresh_db, tmp_db_cfg, monkeypatch
|
||||||
|
):
|
||||||
|
"""Test that _on_tag_added defers the save (lines 1043-1048)"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
# Mock QTimer.singleShot
|
||||||
|
with patch("PySide6.QtCore.QTimer.singleShot") as mock_timer:
|
||||||
|
window._on_tag_added()
|
||||||
|
|
||||||
|
# Should have called singleShot
|
||||||
|
mock_timer.assert_called_once()
|
||||||
|
args = mock_timer.call_args[0]
|
||||||
|
assert args[0] == 0 # Delay of 0
|
||||||
|
assert callable(args[1]) # Callback function
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tag Activation Tests (lines 1070-1080)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_on_tag_activated_with_date(app, fresh_db, tmp_db_cfg, monkeypatch):
|
||||||
|
"""Test _on_tag_activated when passed a date string"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
# Mock _load_selected_date
|
||||||
|
window._load_selected_date = Mock()
|
||||||
|
|
||||||
|
# Call with date format
|
||||||
|
window._on_tag_activated("2024-01-15")
|
||||||
|
|
||||||
|
# Should have called _load_selected_date
|
||||||
|
window._load_selected_date.assert_called_once_with("2024-01-15")
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_on_tag_activated_with_tag_name(
|
||||||
|
app, fresh_db, tmp_db_cfg, monkeypatch
|
||||||
|
):
|
||||||
|
"""Test _on_tag_activated when passed a tag name"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
# Mock the tag browser dialog (it's imported locally in the method)
|
||||||
|
with patch("bouquin.tag_browser.TagBrowserDialog") as mock_dialog:
|
||||||
|
mock_instance = Mock()
|
||||||
|
mock_instance.openDateRequested = Mock()
|
||||||
|
mock_instance.exec.return_value = QDialog.Accepted
|
||||||
|
mock_dialog.return_value = mock_instance
|
||||||
|
|
||||||
|
# Call with tag name
|
||||||
|
window._on_tag_activated("worktag")
|
||||||
|
|
||||||
|
# Should have opened dialog
|
||||||
|
mock_dialog.assert_called_once()
|
||||||
|
# Check focus_tag was passed
|
||||||
|
call_kwargs = mock_dialog.call_args[1]
|
||||||
|
assert call_kwargs.get("focus_tag") == "worktag"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Settings Path Change Tests (lines 1105-1116)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_settings_path_change_success(
|
||||||
|
app, fresh_db, tmp_db_cfg, tmp_path, monkeypatch
|
||||||
|
):
|
||||||
|
"""Test changing database path in settings"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
new_path = tmp_path / "new.db"
|
||||||
|
|
||||||
|
# Mock the settings dialog
|
||||||
|
with patch("bouquin.main_window.SettingsDialog") as mock_dialog:
|
||||||
|
mock_instance = Mock()
|
||||||
|
mock_instance.exec.return_value = QDialog.Accepted
|
||||||
|
|
||||||
|
# Create a new config with different path
|
||||||
|
new_cfg = Mock()
|
||||||
|
new_cfg.path = str(new_path)
|
||||||
|
new_cfg.key = tmp_db_cfg.key
|
||||||
|
new_cfg.idle_minutes = 15
|
||||||
|
new_cfg.theme = "light"
|
||||||
|
new_cfg.move_todos = True
|
||||||
|
new_cfg.locale = "en"
|
||||||
|
|
||||||
|
mock_instance.config = new_cfg
|
||||||
|
mock_dialog.return_value = mock_instance
|
||||||
|
|
||||||
|
# Mock _prompt_for_key_until_valid to return True
|
||||||
|
window._prompt_for_key_until_valid = Mock(return_value=True)
|
||||||
|
# Also mock _load_selected_date and _refresh_calendar_marks since we don't have a real DB connection
|
||||||
|
window._load_selected_date = Mock()
|
||||||
|
window._refresh_calendar_marks = Mock()
|
||||||
|
|
||||||
|
# Open settings
|
||||||
|
window._open_settings()
|
||||||
|
|
||||||
|
# Path should have changed
|
||||||
|
assert window.cfg.path == str(new_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_settings_path_change_failure(
|
||||||
|
app, fresh_db, tmp_db_cfg, tmp_path, monkeypatch
|
||||||
|
):
|
||||||
|
"""Test failed database path change shows warning (lines 1108-1113)"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
new_path = tmp_path / "new.db"
|
||||||
|
|
||||||
|
# Mock the settings dialog
|
||||||
|
with patch("bouquin.main_window.SettingsDialog") as mock_dialog:
|
||||||
|
mock_instance = Mock()
|
||||||
|
mock_instance.exec.return_value = QDialog.Accepted
|
||||||
|
|
||||||
|
new_cfg = Mock()
|
||||||
|
new_cfg.path = str(new_path)
|
||||||
|
new_cfg.key = tmp_db_cfg.key
|
||||||
|
new_cfg.idle_minutes = 15
|
||||||
|
new_cfg.theme = "light"
|
||||||
|
new_cfg.move_todos = True
|
||||||
|
new_cfg.locale = "en"
|
||||||
|
|
||||||
|
mock_instance.config = new_cfg
|
||||||
|
mock_dialog.return_value = mock_instance
|
||||||
|
|
||||||
|
# Mock _prompt_for_key_until_valid to return False (failure)
|
||||||
|
window._prompt_for_key_until_valid = Mock(return_value=False)
|
||||||
|
|
||||||
|
# Mock QMessageBox.warning
|
||||||
|
with patch.object(QMessageBox, "warning") as mock_warning:
|
||||||
|
# Open settings
|
||||||
|
window._open_settings()
|
||||||
|
|
||||||
|
# Warning should have been shown
|
||||||
|
mock_warning.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_settings_no_path_change(app, fresh_db, tmp_db_cfg, monkeypatch):
|
||||||
|
"""Test settings change without path change (lines 1105 condition False)"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
old_path = window.cfg.path
|
||||||
|
|
||||||
|
# Mock the settings dialog
|
||||||
|
with patch("bouquin.main_window.SettingsDialog") as mock_dialog:
|
||||||
|
mock_instance = Mock()
|
||||||
|
mock_instance.exec.return_value = QDialog.Accepted
|
||||||
|
|
||||||
|
# Create config with SAME path
|
||||||
|
new_cfg = Mock()
|
||||||
|
new_cfg.path = old_path
|
||||||
|
new_cfg.key = tmp_db_cfg.key
|
||||||
|
new_cfg.idle_minutes = 20 # Changed
|
||||||
|
new_cfg.theme = "dark" # Changed
|
||||||
|
new_cfg.move_todos = False # Changed
|
||||||
|
new_cfg.locale = "fr" # Changed
|
||||||
|
|
||||||
|
mock_instance.config = new_cfg
|
||||||
|
mock_dialog.return_value = mock_instance
|
||||||
|
|
||||||
|
# Open settings
|
||||||
|
window._open_settings()
|
||||||
|
|
||||||
|
# Settings should be updated but path didn't change
|
||||||
|
assert window.cfg.idle_minutes == 20
|
||||||
|
assert window.cfg.theme == "dark"
|
||||||
|
assert window.cfg.path == old_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_settings_cancelled(app, fresh_db, tmp_db_cfg, monkeypatch):
|
||||||
|
"""Test cancelling settings dialog (line 1085-1086)"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
old_theme = window.cfg.theme
|
||||||
|
|
||||||
|
# Mock the settings dialog to be rejected
|
||||||
|
with patch("bouquin.main_window.SettingsDialog") as mock_dialog:
|
||||||
|
mock_instance = Mock()
|
||||||
|
mock_instance.exec.return_value = QDialog.Rejected
|
||||||
|
mock_dialog.return_value = mock_instance
|
||||||
|
|
||||||
|
# Open settings
|
||||||
|
window._open_settings()
|
||||||
|
|
||||||
|
# Settings should NOT change
|
||||||
|
assert window.cfg.theme == old_theme
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Update Tag Views Tests (lines 1039-1041)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_update_tag_views_for_date(app, fresh_db, tmp_db_cfg, monkeypatch):
|
||||||
|
"""Test _update_tag_views_for_date"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
# Set tags for a date
|
||||||
|
fresh_db.set_tags_for_page("2024-01-15", ["test"])
|
||||||
|
|
||||||
|
# Update tag views
|
||||||
|
window._update_tag_views_for_date("2024-01-15")
|
||||||
|
|
||||||
|
# Tags widget should have been updated
|
||||||
|
assert window.tags._current_date == "2024-01-15"
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_window_update_tag_views_no_tags_widget(
|
||||||
|
app, fresh_db, tmp_db_cfg, monkeypatch
|
||||||
|
):
|
||||||
|
"""Test _update_tag_views_for_date when tags widget doesn't exist"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"bouquin.main_window.KeyPrompt",
|
||||||
|
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
|
||||||
|
)
|
||||||
|
|
||||||
|
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
window = MainWindow(themes)
|
||||||
|
|
||||||
|
# Remove tags widget
|
||||||
|
delattr(window, "tags")
|
||||||
|
|
||||||
|
# Should handle gracefully
|
||||||
|
window._update_tag_views_for_date("2024-01-15")
|
||||||
|
|
||||||
|
assert True
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
||||||
import pytest
|
|
||||||
from bouquin.search import Search
|
from bouquin.search import Search
|
||||||
from PySide6.QtWidgets import QListWidgetItem
|
from PySide6.QtWidgets import QListWidgetItem
|
||||||
|
|
||||||
|
|
@ -80,7 +79,6 @@ def test_make_html_snippet_variants(qtbot, fresh_db):
|
||||||
assert "<b>delta</b>" in frag
|
assert "<b>delta</b>" in frag
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_search_error_path_and_empty_snippet(qtbot, fresh_db, monkeypatch):
|
def test_search_error_path_and_empty_snippet(qtbot, fresh_db, monkeypatch):
|
||||||
s = Search(fresh_db)
|
s = Search(fresh_db)
|
||||||
qtbot.addWidget(s)
|
qtbot.addWidget(s)
|
||||||
|
|
@ -92,7 +90,6 @@ def test_search_error_path_and_empty_snippet(qtbot, fresh_db, monkeypatch):
|
||||||
assert frag == "" and not left and not right
|
assert frag == "" and not left and not right
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_populate_results_shows_both_ellipses(qtbot, fresh_db):
|
def test_populate_results_shows_both_ellipses(qtbot, fresh_db):
|
||||||
s = Search(fresh_db)
|
s = Search(fresh_db)
|
||||||
qtbot.addWidget(s)
|
qtbot.addWidget(s)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from bouquin.db import DBManager, DBConfig
|
from bouquin.db import DBManager, DBConfig
|
||||||
from bouquin.key_prompt import KeyPrompt
|
from bouquin.key_prompt import KeyPrompt
|
||||||
import bouquin.settings_dialog as sd
|
import bouquin.settings_dialog as sd
|
||||||
|
|
@ -10,7 +8,6 @@ from PySide6.QtCore import QTimer
|
||||||
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget, QDialog
|
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget, QDialog
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db, tmp_path):
|
def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db, tmp_path):
|
||||||
# Provide a parent that exposes a real ThemeManager (dialog calls parent().themes.set(...))
|
# Provide a parent that exposes a real ThemeManager (dialog calls parent().themes.set(...))
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
|
|
@ -206,7 +203,6 @@ def test_settings_compact_error(qtbot, app, monkeypatch, tmp_db_cfg, fresh_db):
|
||||||
assert called["text"]
|
assert called["text"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_settings_browse_sets_path(qtbot, app, tmp_path, fresh_db, monkeypatch):
|
def test_settings_browse_sets_path(qtbot, app, tmp_path, fresh_db, monkeypatch):
|
||||||
parent = QWidget()
|
parent = QWidget()
|
||||||
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import types
|
import types
|
||||||
import pytest
|
|
||||||
from PySide6.QtWidgets import QFileDialog
|
from PySide6.QtWidgets import QFileDialog
|
||||||
from PySide6.QtGui import QTextCursor
|
from PySide6.QtGui import QTextCursor
|
||||||
|
|
||||||
|
|
@ -10,7 +9,6 @@ from bouquin.main_window import MainWindow
|
||||||
from bouquin.history_dialog import HistoryDialog
|
from bouquin.history_dialog import HistoryDialog
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_tabs_open_and_deduplicate(qtbot, app, tmp_db_cfg, fresh_db):
|
def test_tabs_open_and_deduplicate(qtbot, app, tmp_db_cfg, fresh_db):
|
||||||
# point to the temp encrypted DB
|
# point to the temp encrypted DB
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
|
|
@ -43,7 +41,6 @@ def test_tabs_open_and_deduplicate(qtbot, app, tmp_db_cfg, fresh_db):
|
||||||
assert w.tab_widget.currentWidget().current_date == date1
|
assert w.tab_widget.currentWidget().current_date == date1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_toolbar_signals_dispatch_once_per_click(
|
def test_toolbar_signals_dispatch_once_per_click(
|
||||||
qtbot, app, tmp_db_cfg, fresh_db, monkeypatch
|
qtbot, app, tmp_db_cfg, fresh_db, monkeypatch
|
||||||
):
|
):
|
||||||
|
|
@ -115,7 +112,6 @@ def test_toolbar_signals_dispatch_once_per_click(
|
||||||
assert calls2["bold"] == 1
|
assert calls2["bold"] == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_history_and_insert_image_not_duplicated(
|
def test_history_and_insert_image_not_duplicated(
|
||||||
qtbot, app, tmp_db_cfg, fresh_db, monkeypatch, tmp_path
|
qtbot, app, tmp_db_cfg, fresh_db, monkeypatch, tmp_path
|
||||||
):
|
):
|
||||||
|
|
@ -158,7 +154,6 @@ def test_history_and_insert_image_not_duplicated(
|
||||||
assert inserted["count"] == 1
|
assert inserted["count"] == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_highlighter_attached_after_text_load(qtbot, app, tmp_db_cfg, fresh_db):
|
def test_highlighter_attached_after_text_load(qtbot, app, tmp_db_cfg, fresh_db):
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
s.setValue("db/path", str(tmp_db_cfg.path))
|
s.setValue("db/path", str(tmp_db_cfg.path))
|
||||||
|
|
@ -174,7 +169,6 @@ def test_highlighter_attached_after_text_load(qtbot, app, tmp_db_cfg, fresh_db):
|
||||||
assert w.editor.highlighter.document() is w.editor.document()
|
assert w.editor.highlighter.document() is w.editor.document()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_findbar_works_for_current_tab(qtbot, app, tmp_db_cfg, fresh_db):
|
def test_findbar_works_for_current_tab(qtbot, app, tmp_db_cfg, fresh_db):
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
s.setValue("db/path", str(tmp_db_cfg.path))
|
s.setValue("db/path", str(tmp_db_cfg.path))
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
||||||
import pytest
|
|
||||||
from PySide6.QtGui import QPalette
|
from PySide6.QtGui import QPalette
|
||||||
from PySide6.QtWidgets import QApplication, QCalendarWidget, QWidget
|
from PySide6.QtWidgets import QApplication, QCalendarWidget, QWidget
|
||||||
|
|
||||||
|
|
@ -15,7 +14,6 @@ def test_theme_manager_apply_light_and_dark(app):
|
||||||
assert isinstance(app.palette(), QPalette)
|
assert isinstance(app.palette(), QPalette)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_theme_manager_system_roundtrip(app, qtbot):
|
def test_theme_manager_system_roundtrip(app, qtbot):
|
||||||
cfg = ThemeConfig(theme=Theme.SYSTEM)
|
cfg = ThemeConfig(theme=Theme.SYSTEM)
|
||||||
mgr = ThemeManager(app, cfg)
|
mgr = ThemeManager(app, cfg)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ def editor(app, qtbot):
|
||||||
return ed
|
return ed
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def test_toolbar_signals_and_styling(qtbot, editor):
|
def test_toolbar_signals_and_styling(qtbot, editor):
|
||||||
host = QWidget()
|
host = QWidget()
|
||||||
qtbot.addWidget(host)
|
qtbot.addWidget(host)
|
||||||
|
|
|
||||||
22
vulture_ignorelist.py
Normal file
22
vulture_ignorelist.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
from bouquin.flow_layout import FlowLayout
|
||||||
|
from bouquin.markdown_editor import MarkdownEditor
|
||||||
|
from bouquin.markdown_highlighter import MarkdownHighlighter
|
||||||
|
from bouquin.db import DBManager
|
||||||
|
|
||||||
|
DBManager.row_factory
|
||||||
|
|
||||||
|
FlowLayout.itemAt
|
||||||
|
FlowLayout.expandingDirections
|
||||||
|
FlowLayout.hasHeightForWidth
|
||||||
|
FlowLayout.heightForWidth
|
||||||
|
|
||||||
|
MarkdownEditor.apply_weight
|
||||||
|
MarkdownEditor.apply_italic
|
||||||
|
MarkdownEditor.apply_strikethrough
|
||||||
|
MarkdownEditor.apply_code
|
||||||
|
MarkdownEditor.apply_heading
|
||||||
|
MarkdownEditor.toggle_bullets
|
||||||
|
MarkdownEditor.toggle_numbers
|
||||||
|
MarkdownEditor.toggle_checkboxes
|
||||||
|
|
||||||
|
MarkdownHighlighter.highlightBlock
|
||||||
Loading…
Add table
Add a link
Reference in a new issue