Prevent traceback on trying to edit a tag with the same name as another tag. Various other tweaks. Bump version
All checks were successful
CI / test (push) Successful in 3m31s
Lint / test (push) Successful in 16s
Trivy / test (push) Successful in 21s

This commit is contained in:
Miguel Jacq 2025-11-14 17:30:58 +11:00
parent 02a60ca656
commit 1becb7900e
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
15 changed files with 153 additions and 83 deletions

View file

@ -1,13 +1,14 @@
# 0.2.1.9 # 0.3
* Fix a few small matters identified with tests * Introduce Tags
* Make locales dynamically detected from the locales dir rather than hardcoded * Make translations dynamically detected from the locales dir rather than hardcoded
* Add Italian translations (thanks @mdaleo404)
* Add version information in the navigation * Add version information in the navigation
* Increase line spacing between lines (except for code blocks) * Increase line spacing between lines (except for code blocks)
* Add Italian translations (thanks @mdaleo404)
* Prevent being able to left-click a date and have it load in current tab if it is already open in another tab * Prevent being able to left-click a date and have it load in current tab if it is already open in another tab
* Avoid second checkbox/bullet on second newline after first newline * Avoid second checkbox/bullet on second newline after first newline
* Avoid Home/left arrow jumping to the left side of a list symbol * Avoid Home/left arrow jumping to the left side of a list symbol
* Various test additions/fixes
# 0.2.1.8 # 0.2.1.8

View file

@ -29,6 +29,7 @@ There is deliberately no network connectivity or syncing intended.
* Tabs are supported - right-click on a date from the calendar to open it in a new tab. * Tabs are supported - right-click on a date from the calendar to open it in a new tab.
* Images are supported * Images are supported
* Search all pages, or find text on page (Ctrl+F) * Search all pages, or find text on page (Ctrl+F)
* Add tags to pages, find pages by tag in the Tag Browser, and customise tag names and colours
* Automatic periodic saving (or explicitly save) * Automatic periodic saving (or explicitly save)
* Transparent integrity checking of the database when it opens * Transparent integrity checking of the database when it opens
* Automatic locking of the app after a period of inactivity (default 15 min) * Automatic locking of the app after a period of inactivity (default 15 min)
@ -37,7 +38,8 @@ There is deliberately no network connectivity or syncing intended.
* Backup the database to encrypted SQLCipher format (which can then be loaded back in to a Bouquin) * Backup the database to encrypted SQLCipher format (which can then be loaded back in to a Bouquin)
* Dark and light themes * Dark and light themes
* Automatically generate checkboxes when typing 'TODO' * Automatically generate checkboxes when typing 'TODO'
* Optionally automatically move unchecked checkboxes from yesterday to today, on startup * It is possible to automatically move unchecked checkboxes from yesterday to today, on startup
* English, French and Italian locales provided
## How to install ## How to install

View file

@ -23,6 +23,20 @@ _TAG_COLORS = [
"#BAFFC9", # soft green "#BAFFC9", # soft green
"#BAE1FF", # soft blue "#BAE1FF", # soft blue
"#E0BAFF", # soft purple "#E0BAFF", # soft purple
"#FFC4B3", # soft coral
"#FFD8B1", # soft peach
"#FFF1BA", # soft light yellow
"#E9FFBA", # soft lime
"#CFFFE5", # soft mint
"#BAFFF5", # soft aqua
"#BAF0FF", # soft cyan
"#C7E9FF", # soft sky blue
"#C7CEFF", # soft periwinkle
"#F0BAFF", # soft lavender pink
"#FFBAF2", # soft magenta
"#FFD1F0", # soft pink
"#EBD5C7", # soft beige
"#EAEAEA", # soft gray
] ]
@ -554,16 +568,22 @@ class DBManager:
name = name.strip() name = name.strip()
color = color.strip() or "#CCCCCC" color = color.strip() or "#CCCCCC"
with self.conn: try:
cur = self.conn.cursor() with self.conn:
cur.execute( cur = self.conn.cursor()
""" cur.execute(
UPDATE tags """
SET name = ?, color = ? UPDATE tags
WHERE id = ?; SET name = ?, color = ?
""", WHERE id = ?;
(name, color, tag_id), """,
) (name, color, tag_id),
)
except sqlite.IntegrityError as e:
if "UNIQUE constraint failed: tags.name" in str(e):
raise sqlite.IntegrityError(
strings._("tag_already_exists_with_that_name")
) from e
def delete_tag(self, tag_id: int) -> None: def delete_tag(self, tag_id: int) -> None:
""" """

View file

@ -112,13 +112,14 @@
"toolbar_heading": "Heading", "toolbar_heading": "Heading",
"toolbar_toggle_checkboxes": "Toggle checkboxes", "toolbar_toggle_checkboxes": "Toggle checkboxes",
"tags": "Tags", "tags": "Tags",
"tag": "Tag",
"manage_tags": "Manage tags", "manage_tags": "Manage tags",
"add_tag_placeholder": "Add a tag and press Enter", "add_tag_placeholder": "Add a tag and press Enter",
"tag_browser_title": "Tag Browser", "tag_browser_title": "Tag Browser",
"tag_browser_instructions": "Click a tag to expand and see all pages with that tag. Click a date to open it. Select a tag to edit its name, change its color, or delete it globally.", "tag_browser_instructions": "Click a tag to expand and see all pages with that tag. Click a date to open it. Select a tag to edit its name, change its color, or delete it globally.",
"tag_name": "Tag name", "tag_name": "Tag name",
"tag_color_hex": "Hex colour", "tag_color_hex": "Hex colour",
"color_hex": "Color", "color_hex": "Colour",
"date": "Date", "date": "Date",
"pick_color": "Pick colour", "pick_color": "Pick colour",
"invalid_color_title": "Invalid colour", "invalid_color_title": "Invalid colour",
@ -130,5 +131,6 @@
"new_tag_name": "New tag name:", "new_tag_name": "New tag name:",
"change_color": "Change colour", "change_color": "Change colour",
"delete_tag": "Delete tag", "delete_tag": "Delete tag",
"delete_tag_confirm": "Are you sure you want to delete the tag '{name}'? This will remove it from all pages." "delete_tag_confirm": "Are you sure you want to delete the tag '{name}'? This will remove it from all pages.",
"tag_already_exists_with_that_name": "A tag already exists with that name"
} }

View file

@ -111,12 +111,13 @@
"toolbar_code_block": "Bloc de code", "toolbar_code_block": "Bloc de code",
"toolbar_heading": "Titre", "toolbar_heading": "Titre",
"toolbar_toggle_checkboxes": "Cocher/Décocher les cases", "toolbar_toggle_checkboxes": "Cocher/Décocher les cases",
"tags": "Tags", "tags": "Étiquettes",
"manage_tags": "Gérer les tags", "tag": "Étiquette",
"add_tag_placeholder": "Ajouter un tag et appuyez sur Entrée", "manage_tags": "Gérer les étiquettes",
"tag_browser_title": "Navigateur de tags", "add_tag_placeholder": "Ajouter une étiquette et appuyez sur Entrée",
"tag_browser_instructions": "Cliquez sur un tag pour l'étendre et voir toutes les pages avec ce tag. Cliquez sur une date pour l'ouvrir. Sélectionnez un tag pour modifier son nom, changer sa couleur ou le supprimer globalement.", "tag_browser_title": "Navigateur de étiquettes",
"tag_name": "Nom du tag", "tag_browser_instructions": "Cliquez sur une étiquette pour l'étendre et voir toutes les pages avec cette étiquette. Cliquez sur une date pour l'ouvrir. Sélectionnez une étiquette pour modifier son nom, changer sa couleur ou la supprimer globalement.",
"tag_name": "Nom de l'étiquette",
"tag_color_hex": "Couleur hexadécimale", "tag_color_hex": "Couleur hexadécimale",
"color_hex": "Couleur", "color_hex": "Couleur",
"date": "Date", "date": "Date",
@ -126,9 +127,10 @@
"add": "Ajouter", "add": "Ajouter",
"remove": "Supprimer", "remove": "Supprimer",
"ok": "OK", "ok": "OK",
"edit_tag_name": "Modifier le nom du tag", "edit_tag_name": "Modifier le nom de l'étiquette",
"new_tag_name": "Nouveau nom du tag :", "new_tag_name": "Nouveau nom de l'étiquette :",
"change_color": "Changer la couleur", "change_color": "Changer la couleur",
"delete_tag": "Supprimer le tag", "delete_tag": "Supprimer l'étiquette",
"delete_tag_confirm": "Êtes-vous sûr de vouloir supprimer le tag '{name}' ? Cela le supprimera de toutes les pages." "delete_tag_confirm": "Êtes-vous sûr de vouloir supprimer l'étiquette '{name}' ? Cela la supprimera de toutes les pages.",
"tag_already_exists_with_that_name": "Une étiquette portant ce nom existe déjà"
} }

View file

@ -130,5 +130,6 @@
"new_tag_name": "Nuovo nome tag:", "new_tag_name": "Nuovo nome tag:",
"change_color": "Cambia colore", "change_color": "Cambia colore",
"delete_tag": "Elimina tag", "delete_tag": "Elimina tag",
"delete_tag_confirm": "Sei sicuro di voler eliminare il tag '{name}'? Questo lo rimuoverà da tutte le pagine." "delete_tag_confirm": "Sei sicuro di voler eliminare il tag '{name}'? Questo lo rimuoverà da tutte le pagine.",
"tag_already_exists_with_that_name": "Esiste già un tag con questo nome"
} }

View file

@ -1077,8 +1077,17 @@ class MainWindow(QMainWindow):
dlg = TagBrowserDialog(self.db, self, focus_tag=tag_name_or_date) dlg = TagBrowserDialog(self.db, self, focus_tag=tag_name_or_date)
dlg.openDateRequested.connect(self._load_selected_date) dlg.openDateRequested.connect(self._load_selected_date)
dlg.tagsModified.connect(self._refresh_current_page_tags)
dlg.exec() dlg.exec()
def _refresh_current_page_tags(self):
"""Refresh the tag chips for the current page (after tag browser changes)"""
if hasattr(self, "tags") and hasattr(self.editor, "current_date"):
date_iso = self.editor.current_date.toString("yyyy-MM-dd")
self.tags.set_current_date(date_iso)
if self.tags.toggle_btn.isChecked():
self.tags._reload_tags()
# ----------- Settings handler ------------# # ----------- Settings handler ------------#
def _open_settings(self): def _open_settings(self):
dlg = SettingsDialog(self.cfg, self.db, self) dlg = SettingsDialog(self.cfg, self.db, self)

View file

@ -199,7 +199,7 @@ class MarkdownHighlighter(QSyntaxHighlighter):
self.setFormat(end - 2, 2, self.syntax_format) self.setFormat(end - 2, 2, self.syntax_format)
self.setFormat(content_start, content_end - content_start, self.bold_format) self.setFormat(content_start, content_end - content_start, self.bold_format)
# --- Italic (*) or (_): skip if it overlaps any triple, keep your guards # --- Italic (*) or (_): skip if it overlaps any triple
for m in re.finditer( for m in re.finditer(
r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)|(?<!_)_(?!_)(.+?)(?<!_)_(?!_)", text r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)|(?<!_)_(?!_)(.+?)(?<!_)_(?!_)", text
): ):

View file

@ -14,11 +14,13 @@ from PySide6.QtWidgets import (
) )
from .db import DBManager from .db import DBManager
from sqlcipher3.dbapi2 import IntegrityError
from . import strings from . import strings
class TagBrowserDialog(QDialog): class TagBrowserDialog(QDialog):
openDateRequested = Signal(str) openDateRequested = Signal(str)
tagsModified = Signal()
def __init__(self, db: DBManager, parent=None, focus_tag: str | None = None): def __init__(self, db: DBManager, parent=None, focus_tag: str | None = None):
super().__init__(parent) super().__init__(parent)
@ -43,6 +45,8 @@ class TagBrowserDialog(QDialog):
self.tree.setColumnWidth(1, 100) self.tree.setColumnWidth(1, 100)
self.tree.itemActivated.connect(self._on_item_activated) self.tree.itemActivated.connect(self._on_item_activated)
self.tree.itemClicked.connect(self._on_item_clicked) self.tree.itemClicked.connect(self._on_item_clicked)
self.tree.setSortingEnabled(True)
self.tree.sortByColumn(0, Qt.AscendingOrder)
layout.addWidget(self.tree) layout.addWidget(self.tree)
# Tag management buttons # Tag management buttons
@ -77,6 +81,9 @@ class TagBrowserDialog(QDialog):
self._populate(focus_tag) self._populate(focus_tag)
def _populate(self, focus_tag: str | None): def _populate(self, focus_tag: str | None):
# Disable sorting during population for better performance
was_sorting = self.tree.isSortingEnabled()
self.tree.setSortingEnabled(False)
self.tree.clear() self.tree.clear()
tags = self._db.list_tags() tags = self._db.list_tags()
focus_item = None focus_item = None
@ -91,7 +98,18 @@ class TagBrowserDialog(QDialog):
) )
# Set background color for the second column to show the tag color # Set background color for the second column to show the tag color
root.setBackground(1, QColor(color)) bg_color = QColor(color)
root.setBackground(1, bg_color)
# Calculate luminance and set contrasting text color
# Using relative luminance formula (ITU-R BT.709)
luminance = (
0.2126 * bg_color.red()
+ 0.7152 * bg_color.green()
+ 0.0722 * bg_color.blue()
) / 255.0
text_color = QColor(0, 0, 0) if luminance > 0.5 else QColor(255, 255, 255)
root.setForeground(1, text_color)
root.setText(1, color) # Also show the hex code root.setText(1, color) # Also show the hex code
root.setTextAlignment(1, Qt.AlignCenter) root.setTextAlignment(1, Qt.AlignCenter)
@ -112,6 +130,9 @@ class TagBrowserDialog(QDialog):
self.tree.expandItem(focus_item) self.tree.expandItem(focus_item)
self.tree.setCurrentItem(focus_item) self.tree.setCurrentItem(focus_item)
# Re-enable sorting after population
self.tree.setSortingEnabled(was_sorting)
def _on_item_clicked(self, item: QTreeWidgetItem, column: int): def _on_item_clicked(self, item: QTreeWidgetItem, column: int):
"""Enable/disable buttons based on selection""" """Enable/disable buttons based on selection"""
data = item.data(0, Qt.ItemDataRole.UserRole) data = item.data(0, Qt.ItemDataRole.UserRole)
@ -156,8 +177,12 @@ class TagBrowserDialog(QDialog):
) )
if ok and new_name and new_name != old_name: if ok and new_name and new_name != old_name:
self._db.update_tag(tag_id, new_name, color) try:
self._populate(None) self._db.update_tag(tag_id, new_name, color)
self._populate(None)
self.tagsModified.emit()
except IntegrityError as e:
QMessageBox.critical(self, strings._("db_database_error"), str(e))
def _change_tag_color(self): def _change_tag_color(self):
"""Change the color of the selected tag""" """Change the color of the selected tag"""
@ -175,8 +200,12 @@ class TagBrowserDialog(QDialog):
color = QColorDialog.getColor(QColor(current_color), self) color = QColorDialog.getColor(QColor(current_color), self)
if color.isValid(): if color.isValid():
self._db.update_tag(tag_id, name, color.name()) try:
self._populate(None) self._db.update_tag(tag_id, name, color.name())
self._populate(None)
self.tagsModified.emit()
except IntegrityError as e:
QMessageBox.critical(self, strings._("db_database_error"), str(e))
def _delete_tag(self): def _delete_tag(self):
"""Delete the selected tag""" """Delete the selected tag"""
@ -203,3 +232,4 @@ class TagBrowserDialog(QDialog):
if reply == QMessageBox.Yes: if reply == QMessageBox.Yes:
self._db.delete_tag(tag_id) self._db.delete_tag(tag_id)
self._populate(None) self._populate(None)
self.tagsModified.emit()

View file

@ -70,7 +70,10 @@ class TagChip(QFrame):
def mouseReleaseEvent(self, ev): def mouseReleaseEvent(self, ev):
if ev.button() == Qt.LeftButton: if ev.button() == Qt.LeftButton:
self.clicked.emit(self._name) self.clicked.emit(self._name)
super().mouseReleaseEvent(ev) try:
super().mouseReleaseEvent(ev)
except RuntimeError:
pass
class PageTagsWidget(QFrame): class PageTagsWidget(QFrame):

View file

@ -204,7 +204,7 @@ class ThemeManager(QObject):
) )
if is_dark: if is_dark:
# Use the link color as the accent (you set this to ORANGE in dark palette) # Use the link color as the accent
accent = pal.color(QPalette.Link) accent = pal.color(QPalette.Link)
r, g, b = accent.red(), accent.green(), accent.blue() r, g, b = accent.red(), accent.green(), accent.blue()
accent_hex = accent.name() accent_hex = accent.name()

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "bouquin" name = "bouquin"
version = "0.2.1.8" version = "0.3"
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
authors = ["Miguel Jacq <mig@mig5.net>"] authors = ["Miguel Jacq <mig@mig5.net>"]
readme = "README.md" readme = "README.md"

View file

@ -435,7 +435,7 @@ def test_init_exits_when_prompt_rejected(app, monkeypatch, tmp_path):
# Avoid accidentaly creating DB by short-circuiting the prompt loop # Avoid accidentaly creating DB by short-circuiting the prompt loop
class MW(MainWindow): class MW(MainWindow):
def _prompt_for_key_until_valid(self, first_time: bool) -> bool: # noqa: N802 def _prompt_for_key_until_valid(self, first_time: bool) -> bool: # noqa: N802
assert first_time is True # hit line 73 path assert first_time is True
return False return False
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
@ -938,7 +938,7 @@ def test_apply_idle_minutes_paths_and_unlock(qtbot, tmp_db_cfg, app, monkeypatch
# remove timer to hit early return # remove timer to hit early return
delattr(w, "_idle_timer") delattr(w, "_idle_timer")
w._apply_idle_minutes(5) # no crash => line 1176 branch w._apply_idle_minutes(5) # no crash
# re-create a timer and simulate locking then disabling idle # re-create a timer and simulate locking then disabling idle
w._idle_timer = QTimer(w) w._idle_timer = QTimer(w)
@ -1474,7 +1474,7 @@ def test_closeEvent_swallows_exceptions(qtbot, app, tmp_db_cfg, monkeypatch):
# ============================================================================ # ============================================================================
# Tag Save Handler Tests (lines 1050-1068) # Tag Save Handler Tests
# ============================================================================ # ============================================================================
@ -1525,7 +1525,7 @@ def test_main_window_do_tag_save_no_editor(app, fresh_db, tmp_db_cfg, monkeypatc
def test_main_window_on_tag_added_triggers_deferred_save( def test_main_window_on_tag_added_triggers_deferred_save(
app, fresh_db, tmp_db_cfg, monkeypatch app, fresh_db, tmp_db_cfg, monkeypatch
): ):
"""Test that _on_tag_added defers the save (lines 1043-1048)""" """Test that _on_tag_added defers the save"""
monkeypatch.setattr( monkeypatch.setattr(
"bouquin.main_window.KeyPrompt", "bouquin.main_window.KeyPrompt",
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key), lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
@ -1546,7 +1546,7 @@ def test_main_window_on_tag_added_triggers_deferred_save(
# ============================================================================ # ============================================================================
# Tag Activation Tests (lines 1070-1080) # Tag Activation Tests
# ============================================================================ # ============================================================================
@ -1600,7 +1600,7 @@ def test_main_window_on_tag_activated_with_tag_name(
# ============================================================================ # ============================================================================
# Settings Path Change Tests (lines 1105-1116) # Settings Path Change Tests
# ============================================================================ # ============================================================================
@ -1651,7 +1651,7 @@ def test_main_window_settings_path_change_success(
def test_main_window_settings_path_change_failure( def test_main_window_settings_path_change_failure(
app, fresh_db, tmp_db_cfg, tmp_path, monkeypatch app, fresh_db, tmp_db_cfg, tmp_path, monkeypatch
): ):
"""Test failed database path change shows warning (lines 1108-1113)""" """Test failed database path change shows warning"""
monkeypatch.setattr( monkeypatch.setattr(
"bouquin.main_window.KeyPrompt", "bouquin.main_window.KeyPrompt",
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key), lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
@ -1691,7 +1691,7 @@ def test_main_window_settings_path_change_failure(
def test_main_window_settings_no_path_change(app, fresh_db, tmp_db_cfg, monkeypatch): 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)""" """Test settings change without path change"""
monkeypatch.setattr( monkeypatch.setattr(
"bouquin.main_window.KeyPrompt", "bouquin.main_window.KeyPrompt",
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key), lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
@ -1729,7 +1729,7 @@ def test_main_window_settings_no_path_change(app, fresh_db, tmp_db_cfg, monkeypa
def test_main_window_settings_cancelled(app, fresh_db, tmp_db_cfg, monkeypatch): def test_main_window_settings_cancelled(app, fresh_db, tmp_db_cfg, monkeypatch):
"""Test cancelling settings dialog (line 1085-1086)""" """Test cancelling settings dialog"""
monkeypatch.setattr( monkeypatch.setattr(
"bouquin.main_window.KeyPrompt", "bouquin.main_window.KeyPrompt",
lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key), lambda *args, **kwargs: Mock(exec=lambda: True, key=lambda: tmp_db_cfg.key),
@ -1753,7 +1753,7 @@ def test_main_window_settings_cancelled(app, fresh_db, tmp_db_cfg, monkeypatch):
# ============================================================================ # ============================================================================
# Update Tag Views Tests (lines 1039-1041) # Update Tag Views Tests
# ============================================================================ # ============================================================================

View file

@ -374,7 +374,7 @@ def test_theme_change_rehighlight(highlighter):
@pytest.fixture @pytest.fixture
def hl_light(app): def hl_light(app):
# Light theme path (covers lines ~74-75 in _on_theme_changed) # Light theme path
tm = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) tm = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
doc = QTextDocument() doc = QTextDocument()
hl = MarkdownHighlighter(doc, tm) hl = MarkdownHighlighter(doc, tm)
@ -435,7 +435,7 @@ def test_code_block_light_colors(hl_light):
def test_end_guard_skips_italic_followed_by_marker(hl_light): def test_end_guard_skips_italic_followed_by_marker(hl_light):
""" """
Triggers the end-following guard for italic (line ~208), e.g. '*i**'. Triggers the end-following guard for italic e.g. '*i**'.
""" """
doc, hl = hl_light doc, hl = hl_light
doc.setPlainText("*i**") doc.setPlainText("*i**")
@ -543,7 +543,7 @@ def test_insert_image_from_path_invalid_returns(editor_hello, tmp_path):
# ============================================================================ # ============================================================================
# setDocument Tests (lines 75-81) # setDocument Tests
# ============================================================================ # ============================================================================
@ -582,7 +582,7 @@ def test_markdown_editor_set_document_with_highlighter(app):
# ============================================================================ # ============================================================================
# showEvent Tests (lines 83-86) # showEvent Tests
# ============================================================================ # ============================================================================
@ -604,7 +604,7 @@ def test_markdown_editor_show_event(app, qtbot):
# ============================================================================ # ============================================================================
# Checkbox Transformation Tests (lines 100-133) # Checkbox Transformation Tests
# ============================================================================ # ============================================================================
@ -645,7 +645,7 @@ def test_markdown_editor_transform_checked_checkbox(app, qtbot):
def test_markdown_editor_transform_todo(app, qtbot): def test_markdown_editor_transform_todo(app, qtbot):
"""Test transforming TODO to unchecked checkbox (lines 110-114)""" """Test transforming TODO to unchecked checkbox"""
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
editor = MarkdownEditor(themes) editor = MarkdownEditor(themes)
editor.show() editor.show()
@ -726,7 +726,7 @@ def test_markdown_editor_no_transform_when_updating(app):
editor.insertPlainText("- [ ] Task") editor.insertPlainText("- [ ] Task")
# Should NOT transform since _updating is True # Should NOT transform since _updating is True
# This tests the early return in _on_text_changed (lines 90-91) # This tests the early return in _on_text_changed
assert editor._updating assert editor._updating
@ -779,7 +779,7 @@ def test_markdown_editor_update_code_block_backgrounds(app):
# ============================================================================ # ============================================================================
# Image Insertion Tests (lines 336-366) # Image Insertion Tests
# ============================================================================ # ============================================================================
@ -811,7 +811,7 @@ def test_markdown_editor_insert_image_from_path(app, tmp_path):
# ============================================================================ # ============================================================================
# Formatting Tests (missing lines in various formatting methods) # Formatting Tests
# ============================================================================ # ============================================================================
@ -886,7 +886,7 @@ def test_markdown_editor_toggle_code_empty_selection(app):
# ============================================================================ # ============================================================================
# Heading Tests (lines 455-459) # Heading Tests
# ============================================================================ # ============================================================================
@ -932,7 +932,7 @@ def test_markdown_editor_set_heading_zero_removes_heading(app):
# ============================================================================ # ============================================================================
# List Tests (lines 483-519) # List Tests
# ============================================================================ # ============================================================================
@ -972,7 +972,7 @@ def test_markdown_editor_toggle_list_ordered(app):
# ============================================================================ # ============================================================================
# Code Block Tests (lines 540-577) # Code Block Tests
# ============================================================================ # ============================================================================
@ -1016,7 +1016,7 @@ def test_markdown_editor_apply_code_remove(app):
# ============================================================================ # ============================================================================
# Checkbox Tests (lines 596-600) # Checkbox Tests
# ============================================================================ # ============================================================================
@ -1032,7 +1032,7 @@ def test_markdown_editor_insert_checkbox_unchecked(app):
# ============================================================================ # ============================================================================
# Toggle Checkboxes Tests (lines 659-660, 686-691) # Toggle Checkboxes Tests
# ============================================================================ # ============================================================================
@ -1071,7 +1071,7 @@ def test_markdown_editor_toggle_checkboxes_mixed(app):
# ============================================================================ # ============================================================================
# Markdown Conversion Tests (lines 703, 710-714, 731) # Markdown Conversion Tests
# ============================================================================ # ============================================================================
@ -1113,7 +1113,7 @@ def test_markdown_editor_from_markdown_with_links(app):
# ============================================================================ # ============================================================================
# Selection and Cursor Tests (lines 747-752) # Selection and Cursor Tests
# ============================================================================ # ============================================================================
@ -1152,7 +1152,7 @@ def test_markdown_editor_get_selected_blocks(app):
# ============================================================================ # ============================================================================
# Key Event Tests (lines 795, 806-809) # Key Event Tests
# ============================================================================ # ============================================================================
@ -1194,7 +1194,7 @@ def test_markdown_editor_key_press_return_in_list(app):
# ============================================================================ # ============================================================================
# Link Handling Tests (lines 898, 922, 949, 990) # Link Handling Tests
# ============================================================================ # ============================================================================
@ -1229,12 +1229,12 @@ def test_markdown_editor_mouse_move_over_link(app):
# ============================================================================ # ============================================================================
# Theme Mode Tests (lines 72-79) # Theme Mode Tests
# ============================================================================ # ============================================================================
def test_markdown_highlighter_light_mode(app): def test_markdown_highlighter_light_mode(app):
"""Test highlighter in light mode (lines 74-77)""" """Test highlighter in light mode"""
doc = QTextDocument() doc = QTextDocument()
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
highlighter = MarkdownHighlighter(doc, themes) highlighter = MarkdownHighlighter(doc, themes)
@ -1252,7 +1252,7 @@ def test_markdown_highlighter_light_mode(app):
def test_markdown_highlighter_dark_mode(app): def test_markdown_highlighter_dark_mode(app):
"""Test highlighter in dark mode (lines 70-71)""" """Test highlighter in dark mode"""
doc = QTextDocument() doc = QTextDocument()
themes = ThemeManager(app, ThemeConfig(theme=Theme.DARK)) themes = ThemeManager(app, ThemeConfig(theme=Theme.DARK))
highlighter = MarkdownHighlighter(doc, themes) highlighter = MarkdownHighlighter(doc, themes)
@ -1266,7 +1266,7 @@ def test_markdown_highlighter_dark_mode(app):
# ============================================================================ # ============================================================================
# Highlighting Pattern Tests (lines 196, 208, 211, 213) # Highlighting Pattern Tests
# ============================================================================ # ============================================================================

View file

@ -786,7 +786,7 @@ def test_tag_page_without_content(fresh_db):
# ============================================================================ # ============================================================================
# TagChip Mouse Event Tests (tags_widget.py lines 70-73) # TagChip Mouse Event Tests
# ============================================================================ # ============================================================================
@ -844,12 +844,12 @@ def test_tag_chip_right_click_no_signal(app, qtbot):
# ============================================================================ # ============================================================================
# PageTagsWidget Edge Cases (tags_widget.py missing lines) # PageTagsWidget Edge Cases
# ============================================================================ # ============================================================================
def test_page_tags_widget_add_tag_with_completer_popup_visible(app, fresh_db): def test_page_tags_widget_add_tag_with_completer_popup_visible(app, fresh_db):
"""Test adding tag when completer popup is visible (line 148)""" """Test adding tag when completer popup is visible"""
widget = PageTagsWidget(fresh_db) widget = PageTagsWidget(fresh_db)
widget.show() widget.show()
date_iso = "2024-01-15" date_iso = "2024-01-15"
@ -906,12 +906,12 @@ def test_page_tags_widget_no_current_date_remove_tag(app, fresh_db):
# ============================================================================ # ============================================================================
# TagBrowserDialog Interactive Tests (tag_browser.py lines 124-126, 139-205) # TagBrowserDialog Interactive Tests
# ============================================================================ # ============================================================================
def test_tag_browser_button_states_with_page_item(app, fresh_db): def test_tag_browser_button_states_with_page_item(app, fresh_db):
"""Test that buttons are disabled when clicking a page item (lines 124-126)""" """Test that buttons are disabled when clicking a page item"""
fresh_db.save_new_version("2024-01-15", "Content", "note") fresh_db.save_new_version("2024-01-15", "Content", "note")
fresh_db.set_tags_for_page("2024-01-15", ["test"]) fresh_db.set_tags_for_page("2024-01-15", ["test"])
@ -936,7 +936,7 @@ def test_tag_browser_button_states_with_page_item(app, fresh_db):
def test_tag_browser_edit_tag_name_no_item(app, fresh_db): def test_tag_browser_edit_tag_name_no_item(app, fresh_db):
"""Test editing tag name when no item is selected (lines 139-141)""" """Test editing tag name when no item is selected"""
dialog = TagBrowserDialog(fresh_db) dialog = TagBrowserDialog(fresh_db)
# Try to edit without selecting anything # Try to edit without selecting anything
@ -947,7 +947,7 @@ def test_tag_browser_edit_tag_name_no_item(app, fresh_db):
def test_tag_browser_edit_tag_name_page_item(app, fresh_db): def test_tag_browser_edit_tag_name_page_item(app, fresh_db):
"""Test editing tag name when a page item is selected (lines 143-145)""" """Test editing tag name when a page item is selected"""
fresh_db.save_new_version("2024-01-15", "Content", "note") fresh_db.save_new_version("2024-01-15", "Content", "note")
fresh_db.set_tags_for_page("2024-01-15", ["test"]) fresh_db.set_tags_for_page("2024-01-15", ["test"])
@ -969,7 +969,7 @@ def test_tag_browser_edit_tag_name_page_item(app, fresh_db):
def test_tag_browser_change_color_no_item(app, fresh_db): def test_tag_browser_change_color_no_item(app, fresh_db):
"""Test changing color when no item is selected (lines 164-166)""" """Test changing color when no item is selected"""
dialog = TagBrowserDialog(fresh_db) dialog = TagBrowserDialog(fresh_db)
# Try to change color without selecting anything # Try to change color without selecting anything
@ -980,7 +980,7 @@ def test_tag_browser_change_color_no_item(app, fresh_db):
def test_tag_browser_change_color_page_item(app, fresh_db): def test_tag_browser_change_color_page_item(app, fresh_db):
"""Test changing color when a page item is selected (lines 168-170)""" """Test changing color when a page item is selected"""
fresh_db.save_new_version("2024-01-15", "Content", "note") fresh_db.save_new_version("2024-01-15", "Content", "note")
fresh_db.set_tags_for_page("2024-01-15", ["test"]) fresh_db.set_tags_for_page("2024-01-15", ["test"])
@ -1002,7 +1002,7 @@ def test_tag_browser_change_color_page_item(app, fresh_db):
def test_tag_browser_delete_tag_no_item(app, fresh_db): def test_tag_browser_delete_tag_no_item(app, fresh_db):
"""Test deleting tag when no item is selected (lines 183-185)""" """Test deleting tag when no item is selected"""
dialog = TagBrowserDialog(fresh_db) dialog = TagBrowserDialog(fresh_db)
# Try to delete without selecting anything # Try to delete without selecting anything
@ -1013,7 +1013,7 @@ def test_tag_browser_delete_tag_no_item(app, fresh_db):
def test_tag_browser_delete_tag_page_item(app, fresh_db): def test_tag_browser_delete_tag_page_item(app, fresh_db):
"""Test deleting tag when a page item is selected (lines 187-189)""" """Test deleting tag when a page item is selected"""
fresh_db.save_new_version("2024-01-15", "Content", "note") fresh_db.save_new_version("2024-01-15", "Content", "note")
fresh_db.set_tags_for_page("2024-01-15", ["test"]) fresh_db.set_tags_for_page("2024-01-15", ["test"])
@ -1036,12 +1036,12 @@ def test_tag_browser_delete_tag_page_item(app, fresh_db):
# ============================================================================ # ============================================================================
# FlowLayout Edge Case (flow_layout.py line 28) # FlowLayout Edge Case
# ============================================================================ # ============================================================================
def test_flow_layout_take_at_out_of_bounds(app): def test_flow_layout_take_at_out_of_bounds(app):
"""Test FlowLayout.takeAt with invalid index (line 28)""" """Test FlowLayout.takeAt with invalid index"""
layout = FlowLayout() layout = FlowLayout()
# Try to take item at index that doesn't exist # Try to take item at index that doesn't exist
@ -1063,7 +1063,7 @@ def test_flow_layout_take_at_negative(app):
# ============================================================================ # ============================================================================
# DB Edge Case (db.py line 434) # DB Edge Case for tags
# ============================================================================ # ============================================================================