Consolidate some code related to opening documents using the Documents feature. More code coverage
This commit is contained in:
parent
25f0c28582
commit
0b76f0b490
9 changed files with 2101 additions and 108 deletions
|
|
@ -1,3 +1,8 @@
|
|||
# 0.6.1
|
||||
|
||||
* Consolidate some code related to opening documents using the Documents feature.
|
||||
* More code coverage
|
||||
|
||||
# 0.6.0
|
||||
|
||||
* Add 'Documents' feature. Documents are tied to Projects in the same way as Time Logging, and can be tagged via the Tags feature.
|
||||
|
|
|
|||
64
bouquin/document_utils.py
Normal file
64
bouquin/document_utils.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"""
|
||||
Utility functions for document operations.
|
||||
|
||||
This module provides shared functionality for document handling across
|
||||
different widgets (TodaysDocumentsWidget, DocumentsDialog, SearchResultsDialog,
|
||||
and TagBrowserDialog).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from PySide6.QtCore import QUrl
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
from PySide6.QtWidgets import QMessageBox, QWidget
|
||||
|
||||
from . import strings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .db import DBManager
|
||||
|
||||
|
||||
def open_document_from_db(
|
||||
db: DBManager, doc_id: int, file_name: str, parent_widget: Optional[QWidget] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Open a document by fetching it from the database and opening with system default app.
|
||||
"""
|
||||
# Fetch document data from database
|
||||
try:
|
||||
data = db.document_data(doc_id)
|
||||
except Exception as e:
|
||||
# Show error dialog if parent widget is provided
|
||||
if parent_widget:
|
||||
QMessageBox.warning(
|
||||
parent_widget,
|
||||
strings._("project_documents_title"),
|
||||
strings._("documents_open_failed").format(error=str(e)),
|
||||
)
|
||||
return False
|
||||
|
||||
# Extract file extension
|
||||
suffix = Path(file_name).suffix or ""
|
||||
|
||||
# Create temporary file with same extension
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
prefix="bouquin_doc_",
|
||||
suffix=suffix,
|
||||
delete=False,
|
||||
)
|
||||
|
||||
# Write data to temp file
|
||||
try:
|
||||
tmp.write(data)
|
||||
tmp.flush()
|
||||
finally:
|
||||
tmp.close()
|
||||
|
||||
# Open with system default application
|
||||
success = QDesktopServices.openUrl(QUrl.fromLocalFile(tmp.name))
|
||||
|
||||
return success
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import Qt, QUrl
|
||||
from PySide6.QtGui import QDesktopServices, QColor
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
|
|
@ -147,29 +145,9 @@ class TodaysDocumentsWidget(QFrame):
|
|||
|
||||
def _open_document(self, doc_id: int, file_name: str) -> None:
|
||||
"""Open a document from the list."""
|
||||
try:
|
||||
data = self._db.document_data(doc_id)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
strings._("project_documents_title"),
|
||||
strings._("documents_open_failed").format(error=str(e)),
|
||||
)
|
||||
return
|
||||
from .document_utils import open_document_from_db
|
||||
|
||||
suffix = Path(file_name).suffix or ""
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
prefix="bouquin_doc_",
|
||||
suffix=suffix,
|
||||
delete=False,
|
||||
)
|
||||
try:
|
||||
tmp.write(data)
|
||||
tmp.flush()
|
||||
finally:
|
||||
tmp.close()
|
||||
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(tmp.name))
|
||||
open_document_from_db(self._db, doc_id, file_name, parent_widget=self)
|
||||
|
||||
def _open_documents_dialog(self) -> None:
|
||||
"""Open the full DocumentsDialog."""
|
||||
|
|
@ -553,29 +531,9 @@ class DocumentsDialog(QDialog):
|
|||
"""
|
||||
Fetch BLOB from DB, write to a temporary file, and open with default app.
|
||||
"""
|
||||
try:
|
||||
data = self._db.document_data(doc_id)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
strings._("project_documents_title"),
|
||||
strings._("documents_open_failed").format(error=str(e)),
|
||||
)
|
||||
return
|
||||
from .document_utils import open_document_from_db
|
||||
|
||||
suffix = Path(file_name).suffix or ""
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
prefix="bouquin_doc_",
|
||||
suffix=suffix,
|
||||
delete=False,
|
||||
)
|
||||
try:
|
||||
tmp.write(data)
|
||||
tmp.flush()
|
||||
finally:
|
||||
tmp.close()
|
||||
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(tmp.name))
|
||||
open_document_from_db(self._db, doc_id, file_name, parent_widget=self)
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size_bytes: int) -> str:
|
||||
|
|
|
|||
|
|
@ -69,38 +69,10 @@ class Search(QWidget):
|
|||
self._open_document(int(doc_id), file_name)
|
||||
|
||||
def _open_document(self, doc_id: int, file_name: str) -> None:
|
||||
"""
|
||||
Open a document search result via a temp file.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from PySide6.QtCore import QUrl
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
"""Open the selected document in the user's default app."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
try:
|
||||
data = self._db.document_data(doc_id)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
strings._("project_documents_title"),
|
||||
strings._("documents_open_failed").format(error=str(e)),
|
||||
)
|
||||
return
|
||||
|
||||
suffix = Path(file_name).suffix or ""
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
prefix="bouquin_doc_",
|
||||
suffix=suffix,
|
||||
delete=False,
|
||||
)
|
||||
try:
|
||||
tmp.write(data)
|
||||
tmp.flush()
|
||||
finally:
|
||||
tmp.close()
|
||||
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(tmp.name))
|
||||
open_document_from_db(self._db, doc_id, file_name, parent_widget=self)
|
||||
|
||||
def _search(self, text: str):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from PySide6.QtCore import Qt, Signal, QUrl
|
||||
from PySide6.QtGui import QColor, QDesktopServices
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
|
|
@ -13,9 +13,6 @@ from PySide6.QtWidgets import (
|
|||
QInputDialog,
|
||||
)
|
||||
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
from .db import DBManager
|
||||
from .settings import load_db_config
|
||||
from . import strings
|
||||
|
|
@ -197,30 +194,10 @@ class TagBrowserDialog(QDialog):
|
|||
self._open_document(int(doc_id), str(data.get("file_name")))
|
||||
|
||||
def _open_document(self, doc_id: int, file_name: str) -> None:
|
||||
"""Open a tagged document via the default external application."""
|
||||
try:
|
||||
data = self._db.document_data(doc_id)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
strings._("project_documents_title"),
|
||||
strings._("documents_open_failed").format(error=str(e)),
|
||||
)
|
||||
return
|
||||
"""Open a tagged document from the list."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
suffix = Path(file_name).suffix or ""
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
prefix="bouquin_doc_",
|
||||
suffix=suffix,
|
||||
delete=False,
|
||||
)
|
||||
try:
|
||||
tmp.write(data)
|
||||
tmp.flush()
|
||||
finally:
|
||||
tmp.close()
|
||||
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(tmp.name))
|
||||
open_document_from_db(self._db, doc_id, file_name, parent_widget=self)
|
||||
|
||||
def _add_a_tag(self):
|
||||
"""Add a new tag"""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
from PySide6.QtWidgets import QPushButton
|
||||
from bouquin.code_block_editor_dialog import CodeBlockEditorDialog
|
||||
from bouquin import strings
|
||||
|
||||
from PySide6.QtCore import QRect, QSize
|
||||
from PySide6.QtGui import QPaintEvent, QFont
|
||||
|
||||
from bouquin.code_block_editor_dialog import (
|
||||
CodeBlockEditorDialog,
|
||||
CodeEditorWithLineNumbers,
|
||||
)
|
||||
|
||||
|
||||
def _find_button_by_text(widget, text):
|
||||
for btn in widget.findChildren(QPushButton):
|
||||
|
|
@ -29,3 +36,292 @@ def test_code_block_dialog_language_and_code(qtbot):
|
|||
assert _find_button_by_text(dlg, delete_txt) is None
|
||||
assert dlg.code() == "x = 1"
|
||||
assert dlg.language() is None
|
||||
|
||||
|
||||
def test_line_number_area_size_hint(qtbot, app):
|
||||
"""Test _LineNumberArea.sizeHint() method."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
|
||||
line_area = editor._line_number_area
|
||||
size_hint = line_area.sizeHint()
|
||||
|
||||
# Should return a QSize with width from editor
|
||||
assert isinstance(size_hint, QSize)
|
||||
assert size_hint.width() > 0
|
||||
assert size_hint.height() == 0
|
||||
|
||||
|
||||
def test_line_number_area_paint_event(qtbot, app):
|
||||
"""Test _LineNumberArea.paintEvent() method."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
editor.setPlainText("Line 1\nLine 2\nLine 3")
|
||||
editor.show()
|
||||
|
||||
# Trigger a paint event on the line number area
|
||||
line_area = editor._line_number_area
|
||||
paint_event = QPaintEvent(QRect(0, 0, line_area.width(), line_area.height()))
|
||||
line_area.paintEvent(paint_event)
|
||||
|
||||
# Should not crash
|
||||
|
||||
|
||||
def test_line_number_font_pixel_size_fallback(qtbot, app):
|
||||
"""Test _line_number_font() with pixel-sized font."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
|
||||
# Set a pixel-sized font (pointSize will be -1)
|
||||
font = QFont()
|
||||
font.setPixelSize(14)
|
||||
editor.setFont(font)
|
||||
|
||||
# Get line number font - should use the fallback
|
||||
line_font = editor._line_number_font()
|
||||
|
||||
# Should have calculated a size
|
||||
assert line_font.pointSizeF() > 0 or line_font.pixelSize() > 0
|
||||
|
||||
|
||||
def test_code_editor_resize_event(qtbot, app):
|
||||
"""Test CodeEditorWithLineNumbers.resizeEvent() method."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
editor.show()
|
||||
|
||||
# Resize the editor
|
||||
editor.resize(400, 300)
|
||||
|
||||
# Line number area should be repositioned
|
||||
line_area = editor._line_number_area
|
||||
assert line_area.geometry().width() > 0
|
||||
assert line_area.geometry().height() == editor.contentsRect().height()
|
||||
|
||||
|
||||
def test_code_editor_update_with_scroll(qtbot, app):
|
||||
"""Test _update_line_number_area with dy (scroll) parameter."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
|
||||
# Add enough text to enable scrolling
|
||||
text = "\n".join([f"Line {i}" for i in range(100)])
|
||||
editor.setPlainText(text)
|
||||
editor.show()
|
||||
|
||||
# Trigger update with scroll offset
|
||||
rect = QRect(0, 0, 100, 100)
|
||||
editor._update_line_number_area(rect, dy=10)
|
||||
|
||||
# Should not crash
|
||||
|
||||
|
||||
def test_code_editor_update_without_scroll(qtbot, app):
|
||||
"""Test _update_line_number_area without scroll (dy=0)."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
editor.setPlainText("Line 1\nLine 2")
|
||||
editor.show()
|
||||
|
||||
# Trigger update without scroll
|
||||
rect = QRect(0, 0, 100, 100)
|
||||
editor._update_line_number_area(rect, dy=0)
|
||||
|
||||
# Should not crash
|
||||
|
||||
|
||||
def test_code_editor_update_contains_viewport(qtbot, app):
|
||||
"""Test _update_line_number_area when rect contains viewport."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
editor.setPlainText("Test")
|
||||
editor.show()
|
||||
|
||||
# Trigger update with rect that contains viewport
|
||||
viewport_rect = editor.viewport().rect()
|
||||
editor._update_line_number_area(viewport_rect, dy=0)
|
||||
|
||||
# Should trigger width update (covers line 82)
|
||||
|
||||
|
||||
def test_line_number_area_paint_with_multiple_blocks(qtbot, app):
|
||||
"""Test line_number_area_paint_event with multiple text blocks."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
|
||||
# Add multiple lines
|
||||
text = "\n".join([f"Line {i}" for i in range(20)])
|
||||
editor.setPlainText(text)
|
||||
editor.show()
|
||||
|
||||
# Force a paint event
|
||||
line_area = editor._line_number_area
|
||||
rect = QRect(0, 0, line_area.width(), line_area.height())
|
||||
paint_event = QPaintEvent(rect)
|
||||
|
||||
# This should exercise the painting loop (lines 87-130)
|
||||
editor.line_number_area_paint_event(paint_event)
|
||||
|
||||
# Should not crash
|
||||
|
||||
|
||||
def test_line_number_area_paint_with_long_file(qtbot, app):
|
||||
"""Test line_number_area_paint_event with many lines."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
|
||||
# Add 1000+ lines to test digit calculation and painting
|
||||
text = "\n".join([f"Line {i}" for i in range(1000)])
|
||||
editor.setPlainText(text)
|
||||
editor.show()
|
||||
|
||||
# Trigger paint event
|
||||
line_area = editor._line_number_area
|
||||
paint_event = QPaintEvent(line_area.rect())
|
||||
editor.line_number_area_paint_event(paint_event)
|
||||
|
||||
# Line number width should accommodate 4 digits
|
||||
width = editor.line_number_area_width()
|
||||
assert width > 30 # Should be wider for 4-digit numbers
|
||||
|
||||
|
||||
def test_code_block_editor_dialog_with_delete(qtbot, app):
|
||||
"""Test CodeBlockEditorDialog with allow_delete=True."""
|
||||
dialog = CodeBlockEditorDialog("print('hello')", "python", allow_delete=True)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should have delete button functionality
|
||||
assert hasattr(dialog, "_delete_requested")
|
||||
assert dialog._delete_requested is False
|
||||
|
||||
# Simulate delete click
|
||||
dialog._on_delete_clicked()
|
||||
|
||||
assert dialog._delete_requested is True
|
||||
assert dialog.was_deleted() is True
|
||||
|
||||
|
||||
def test_code_block_editor_dialog_without_delete(qtbot, app):
|
||||
"""Test CodeBlockEditorDialog with allow_delete=False."""
|
||||
dialog = CodeBlockEditorDialog("print('hello')", "python", allow_delete=False)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should not have been deleted
|
||||
assert dialog.was_deleted() is False
|
||||
|
||||
|
||||
def test_code_block_editor_dialog_language_selection(qtbot, app):
|
||||
"""Test language selection in dialog."""
|
||||
dialog = CodeBlockEditorDialog("test", "javascript")
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should have selected javascript
|
||||
assert dialog.language() == "javascript"
|
||||
|
||||
# Change language
|
||||
dialog._lang_combo.setCurrentText("python")
|
||||
assert dialog.language() == "python"
|
||||
|
||||
# Empty language
|
||||
dialog._lang_combo.setCurrentText("")
|
||||
assert dialog.language() is None
|
||||
|
||||
|
||||
def test_code_block_editor_dialog_code_retrieval(qtbot, app):
|
||||
"""Test getting code from dialog."""
|
||||
original_code = "def foo():\n pass"
|
||||
dialog = CodeBlockEditorDialog(original_code, None)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should return the code
|
||||
assert dialog.code() == original_code
|
||||
|
||||
# Modify code
|
||||
new_code = "def bar():\n return 42"
|
||||
dialog._code_edit.setPlainText(new_code)
|
||||
assert dialog.code() == new_code
|
||||
|
||||
|
||||
def test_code_editor_with_empty_text(qtbot, app):
|
||||
"""Test editor with no text."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
editor.show()
|
||||
|
||||
# Should still paint line numbers
|
||||
line_area = editor._line_number_area
|
||||
paint_event = QPaintEvent(line_area.rect())
|
||||
editor.line_number_area_paint_event(paint_event)
|
||||
|
||||
# Should not crash
|
||||
|
||||
|
||||
def test_code_editor_block_count_changed(qtbot, app):
|
||||
"""Test that block count changes trigger width updates."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
|
||||
initial_width = editor.line_number_area_width()
|
||||
|
||||
# Add lots of lines (should require more digits)
|
||||
text = "\n".join([f"Line {i}" for i in range(1000)])
|
||||
editor.setPlainText(text)
|
||||
|
||||
new_width = editor.line_number_area_width()
|
||||
|
||||
# Width should increase for more digits
|
||||
assert new_width > initial_width
|
||||
|
||||
|
||||
def test_code_editor_cursor_position_changed(qtbot, app):
|
||||
"""Test that cursor position changes update line number area."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
editor.setPlainText("Line 1\nLine 2\nLine 3")
|
||||
editor.show()
|
||||
|
||||
# Move cursor
|
||||
cursor = editor.textCursor()
|
||||
cursor.movePosition(cursor.MoveOperation.End)
|
||||
editor.setTextCursor(cursor)
|
||||
|
||||
# Should trigger line number area update (via signal connection)
|
||||
# Just verify it doesn't crash
|
||||
|
||||
|
||||
def test_line_number_area_width_calculation(qtbot, app):
|
||||
"""Test line number area width calculation with various block counts."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
|
||||
# Test with 1 line (should use minimum 2 digits)
|
||||
editor.setPlainText("One line")
|
||||
width_1 = editor.line_number_area_width()
|
||||
assert width_1 > 0
|
||||
|
||||
# Test with 10 lines (2 digits)
|
||||
editor.setPlainText("\n".join(["Line"] * 10))
|
||||
width_10 = editor.line_number_area_width()
|
||||
assert width_10 >= width_1
|
||||
|
||||
# Test with 100 lines (3 digits)
|
||||
editor.setPlainText("\n".join(["Line"] * 100))
|
||||
width_100 = editor.line_number_area_width()
|
||||
assert width_100 > width_10
|
||||
|
||||
|
||||
def test_code_editor_viewport_margins(qtbot, app):
|
||||
"""Test that viewport margins are set correctly."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
editor.setPlainText("Test")
|
||||
editor.show()
|
||||
|
||||
# Left margin should equal line number area width
|
||||
margins = editor.viewportMargins()
|
||||
line_width = editor.line_number_area_width()
|
||||
|
||||
assert margins.left() == line_width
|
||||
assert margins.top() == 0
|
||||
assert margins.right() == 0
|
||||
assert margins.bottom() == 0
|
||||
|
|
|
|||
289
tests/test_document_utils.py
Normal file
289
tests/test_document_utils.py
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
from unittest.mock import patch
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
from PySide6.QtCore import QUrl
|
||||
from PySide6.QtWidgets import QMessageBox, QWidget
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
|
||||
|
||||
def test_open_document_from_db_success(qtbot, app, fresh_db):
|
||||
"""Test successfully opening a document."""
|
||||
# Import here to avoid circular import issues
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
# Add a project and document
|
||||
proj_id = fresh_db.add_project("Test Project")
|
||||
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
||||
doc_path.write_text("test content for document")
|
||||
|
||||
try:
|
||||
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
||||
|
||||
# Mock QDesktopServices.openUrl
|
||||
with patch.object(QDesktopServices, "openUrl", return_value=True) as mock_open:
|
||||
# Call the function
|
||||
success = open_document_from_db(
|
||||
fresh_db, doc_id, doc_path.name, parent_widget=None
|
||||
)
|
||||
|
||||
# Verify success
|
||||
assert success is True
|
||||
|
||||
# Verify openUrl was called with a QUrl
|
||||
assert mock_open.called
|
||||
args = mock_open.call_args[0]
|
||||
assert isinstance(args[0], QUrl)
|
||||
|
||||
# Verify the URL points to a local file
|
||||
url_string = args[0].toString()
|
||||
assert url_string.startswith("file://")
|
||||
assert "bouquin_doc_" in url_string
|
||||
assert doc_path.suffix in url_string
|
||||
finally:
|
||||
doc_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_open_document_from_db_with_parent_widget(qtbot, app, fresh_db):
|
||||
"""Test opening a document with a parent widget provided."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
# Create a parent widget
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
|
||||
# Add a project and document
|
||||
proj_id = fresh_db.add_project("Test Project")
|
||||
doc_path = Path(tempfile.mktemp(suffix=".pdf"))
|
||||
doc_path.write_text("PDF content")
|
||||
|
||||
try:
|
||||
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
||||
|
||||
with patch.object(QDesktopServices, "openUrl", return_value=True) as mock_open:
|
||||
success = open_document_from_db(
|
||||
fresh_db, doc_id, doc_path.name, parent_widget=parent
|
||||
)
|
||||
|
||||
assert success is True
|
||||
assert mock_open.called
|
||||
finally:
|
||||
doc_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_open_document_from_db_nonexistent_document(qtbot, app, fresh_db):
|
||||
"""Test opening a non-existent document returns False."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
# Try to open a document that doesn't exist
|
||||
success = open_document_from_db(
|
||||
fresh_db, doc_id=99999, file_name="nonexistent.txt", parent_widget=None
|
||||
)
|
||||
|
||||
# Should return False
|
||||
assert success is False
|
||||
|
||||
|
||||
def test_open_document_from_db_shows_error_with_parent(qtbot, app, fresh_db):
|
||||
"""Test that error dialog is shown when parent widget is provided."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
|
||||
# Mock QMessageBox.warning
|
||||
with patch.object(QMessageBox, "warning") as mock_warning:
|
||||
success = open_document_from_db(
|
||||
fresh_db, doc_id=99999, file_name="nonexistent.txt", parent_widget=parent
|
||||
)
|
||||
|
||||
# Should return False and show warning
|
||||
assert success is False
|
||||
assert mock_warning.called
|
||||
|
||||
# Verify warning was shown with correct parent
|
||||
call_args = mock_warning.call_args[0]
|
||||
assert call_args[0] is parent
|
||||
|
||||
|
||||
def test_open_document_from_db_no_error_dialog_without_parent(qtbot, app, fresh_db):
|
||||
"""Test that no error dialog is shown when parent widget is None."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
with patch.object(QMessageBox, "warning") as mock_warning:
|
||||
success = open_document_from_db(
|
||||
fresh_db, doc_id=99999, file_name="nonexistent.txt", parent_widget=None
|
||||
)
|
||||
|
||||
# Should return False but NOT show warning
|
||||
assert success is False
|
||||
assert not mock_warning.called
|
||||
|
||||
|
||||
def test_open_document_from_db_preserves_file_extension(qtbot, app, fresh_db):
|
||||
"""Test that the temporary file has the correct extension."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
# Test various file extensions
|
||||
extensions = [".txt", ".pdf", ".docx", ".xlsx", ".jpg", ".png"]
|
||||
|
||||
for ext in extensions:
|
||||
proj_id = fresh_db.add_project(f"Project for {ext}")
|
||||
doc_path = Path(tempfile.mktemp(suffix=ext))
|
||||
doc_path.write_text(f"content for {ext}")
|
||||
|
||||
try:
|
||||
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
||||
|
||||
with patch.object(
|
||||
QDesktopServices, "openUrl", return_value=True
|
||||
) as mock_open:
|
||||
open_document_from_db(fresh_db, doc_id, doc_path.name)
|
||||
|
||||
# Get the URL that was opened
|
||||
url = mock_open.call_args[0][0]
|
||||
url_string = url.toString()
|
||||
|
||||
# Verify the extension is preserved
|
||||
assert ext in url_string, f"Extension {ext} not found in {url_string}"
|
||||
finally:
|
||||
doc_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_open_document_from_db_file_without_extension(qtbot, app, fresh_db):
|
||||
"""Test opening a document without a file extension."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
proj_id = fresh_db.add_project("Test Project")
|
||||
doc_path = Path(tempfile.mktemp()) # No suffix
|
||||
doc_path.write_text("content without extension")
|
||||
|
||||
try:
|
||||
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
||||
|
||||
with patch.object(QDesktopServices, "openUrl", return_value=True) as mock_open:
|
||||
success = open_document_from_db(fresh_db, doc_id, doc_path.name)
|
||||
|
||||
# Should still succeed
|
||||
assert success is True
|
||||
assert mock_open.called
|
||||
finally:
|
||||
doc_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_open_document_from_db_desktop_services_failure(qtbot, app, fresh_db):
|
||||
"""Test handling when QDesktopServices.openUrl returns False."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
proj_id = fresh_db.add_project("Test Project")
|
||||
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
||||
doc_path.write_text("test content")
|
||||
|
||||
try:
|
||||
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
||||
|
||||
# Mock openUrl to return False (failure)
|
||||
with patch.object(QDesktopServices, "openUrl", return_value=False):
|
||||
success = open_document_from_db(fresh_db, doc_id, doc_path.name)
|
||||
|
||||
# Should return False
|
||||
assert success is False
|
||||
finally:
|
||||
doc_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_open_document_from_db_binary_content(qtbot, app, fresh_db):
|
||||
"""Test opening a document with binary content."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
proj_id = fresh_db.add_project("Test Project")
|
||||
doc_path = Path(tempfile.mktemp(suffix=".bin"))
|
||||
|
||||
# Write some binary data
|
||||
binary_data = bytes([0, 1, 2, 3, 255, 254, 253])
|
||||
doc_path.write_bytes(binary_data)
|
||||
|
||||
try:
|
||||
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
||||
|
||||
with patch.object(QDesktopServices, "openUrl", return_value=True) as mock_open:
|
||||
success = open_document_from_db(fresh_db, doc_id, doc_path.name)
|
||||
|
||||
assert success is True
|
||||
assert mock_open.called
|
||||
finally:
|
||||
doc_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_open_document_from_db_large_file(qtbot, app, fresh_db):
|
||||
"""Test opening a large document."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
proj_id = fresh_db.add_project("Test Project")
|
||||
doc_path = Path(tempfile.mktemp(suffix=".bin"))
|
||||
|
||||
# Create a 1MB file
|
||||
large_data = b"x" * (1024 * 1024)
|
||||
doc_path.write_bytes(large_data)
|
||||
|
||||
try:
|
||||
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
||||
|
||||
with patch.object(QDesktopServices, "openUrl", return_value=True) as mock_open:
|
||||
success = open_document_from_db(fresh_db, doc_id, doc_path.name)
|
||||
|
||||
assert success is True
|
||||
assert mock_open.called
|
||||
finally:
|
||||
doc_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_open_document_from_db_temp_file_prefix(qtbot, app, fresh_db):
|
||||
"""Test that temporary files have the correct prefix."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
proj_id = fresh_db.add_project("Test Project")
|
||||
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
||||
doc_path.write_text("test")
|
||||
|
||||
try:
|
||||
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
||||
|
||||
with patch.object(QDesktopServices, "openUrl", return_value=True) as mock_open:
|
||||
open_document_from_db(fresh_db, doc_id, doc_path.name)
|
||||
|
||||
url = mock_open.call_args[0][0]
|
||||
url_path = url.toLocalFile()
|
||||
|
||||
# Verify the temp file has the bouquin_doc_ prefix
|
||||
assert "bouquin_doc_" in url_path
|
||||
finally:
|
||||
doc_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_open_document_from_db_multiple_calls(qtbot, app, fresh_db):
|
||||
"""Test opening the same document multiple times."""
|
||||
from bouquin.document_utils import open_document_from_db
|
||||
|
||||
proj_id = fresh_db.add_project("Test Project")
|
||||
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
||||
doc_path.write_text("test content")
|
||||
|
||||
try:
|
||||
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
||||
|
||||
with patch.object(QDesktopServices, "openUrl", return_value=True) as mock_open:
|
||||
# Open the same document 3 times
|
||||
for _ in range(3):
|
||||
success = open_document_from_db(fresh_db, doc_id, doc_path.name)
|
||||
assert success is True
|
||||
|
||||
# Should have been called 3 times
|
||||
assert mock_open.call_count == 3
|
||||
|
||||
# Each call should create a different temp file
|
||||
call_urls = [call[0][0].toString() for call in mock_open.call_args_list]
|
||||
# All URLs should be different (different temp files)
|
||||
assert len(set(call_urls)) == 3
|
||||
finally:
|
||||
doc_path.unlink(missing_ok=True)
|
||||
1061
tests/test_documents.py
Normal file
1061
tests/test_documents.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -5,6 +5,7 @@ from PySide6.QtWidgets import (
|
|||
QMessageBox,
|
||||
QInputDialog,
|
||||
QFileDialog,
|
||||
QDialog,
|
||||
)
|
||||
from sqlcipher3.dbapi2 import IntegrityError
|
||||
|
||||
|
|
@ -17,6 +18,8 @@ from bouquin.time_log import (
|
|||
)
|
||||
import bouquin.strings as strings
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def theme_manager(app):
|
||||
|
|
@ -2562,3 +2565,371 @@ def test_time_log_with_entry(qtbot, fresh_db):
|
|||
|
||||
# Widget should have been created successfully
|
||||
assert widget is not None
|
||||
|
||||
|
||||
def test_time_log_widget_open_dialog_log_only_when_no_date(qtbot, app, fresh_db):
|
||||
"""Test _open_dialog_log_only when _current_date is None."""
|
||||
widget = TimeLogWidget(fresh_db, themes=None)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
# Set current date to None
|
||||
widget._current_date = None
|
||||
|
||||
# Click should return early without crashing
|
||||
widget._open_dialog_log_only()
|
||||
|
||||
# No dialog should be shown
|
||||
|
||||
|
||||
def test_time_log_widget_open_dialog_log_only_opens_dialog(qtbot, app, fresh_db):
|
||||
"""Test _open_dialog_log_only opens TimeLogDialog."""
|
||||
widget = TimeLogWidget(fresh_db, themes=None)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
# Set a valid date
|
||||
widget._current_date = "2024-01-15"
|
||||
|
||||
# Mock TimeLogDialog
|
||||
mock_dialog = MagicMock()
|
||||
mock_dialog.exec.return_value = QDialog.Accepted
|
||||
|
||||
with patch("bouquin.time_log.TimeLogDialog", return_value=mock_dialog):
|
||||
widget._open_dialog_log_only()
|
||||
|
||||
# Dialog should have been created with correct parameters
|
||||
assert mock_dialog.exec.called
|
||||
|
||||
|
||||
def test_time_log_widget_open_dialog_log_only_refreshes_when_collapsed(
|
||||
qtbot, app, fresh_db
|
||||
):
|
||||
"""Test that opening dialog updates summary when widget is collapsed."""
|
||||
widget = TimeLogWidget(fresh_db, themes=None)
|
||||
qtbot.addWidget(widget)
|
||||
widget._current_date = "2024-01-15"
|
||||
|
||||
# Collapse the widget
|
||||
widget.toggle_btn.setChecked(False)
|
||||
|
||||
# Mock TimeLogDialog
|
||||
mock_dialog = MagicMock()
|
||||
mock_dialog.exec.return_value = QDialog.Accepted
|
||||
|
||||
with patch("bouquin.time_log.TimeLogDialog", return_value=mock_dialog):
|
||||
widget._open_dialog_log_only()
|
||||
|
||||
# Should show collapsed hint
|
||||
assert (
|
||||
"collapsed" in widget.summary_label.text().lower()
|
||||
or widget.summary_label.text() != ""
|
||||
)
|
||||
|
||||
|
||||
def test_time_log_dialog_log_entry_only_mode(qtbot, app, fresh_db):
|
||||
"""Test TimeLogDialog in log_entry_only mode."""
|
||||
dialog = TimeLogDialog(
|
||||
fresh_db, "2024-01-15", log_entry_only=True, themes=None, close_after_add=True
|
||||
)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# In log_entry_only mode, these should be hidden
|
||||
assert not dialog.delete_btn.isVisible()
|
||||
assert not dialog.report_btn.isVisible()
|
||||
assert not dialog.table.isVisible()
|
||||
|
||||
|
||||
def test_time_log_dialog_log_entry_only_false(qtbot, app, fresh_db):
|
||||
"""Test TimeLogDialog in normal mode (log_entry_only=False)."""
|
||||
dialog = TimeLogDialog(
|
||||
fresh_db, "2024-01-15", log_entry_only=False, themes=None, close_after_add=False
|
||||
)
|
||||
qtbot.addWidget(dialog)
|
||||
dialog.show()
|
||||
qtbot.waitExposed(dialog)
|
||||
|
||||
# In normal mode, these should be visible
|
||||
assert dialog.delete_btn.isVisible()
|
||||
assert dialog.report_btn.isVisible()
|
||||
assert dialog.table.isVisible()
|
||||
|
||||
|
||||
def test_time_log_dialog_change_date_cancelled(qtbot, app, fresh_db):
|
||||
"""Test _on_change_date_clicked when user cancels."""
|
||||
dialog = TimeLogDialog(fresh_db, "2024-01-15", themes=None)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Mock exec to return rejected
|
||||
with patch.object(QDialog, "exec", return_value=QDialog.DialogCode.Rejected):
|
||||
original_date = dialog._date_iso
|
||||
dialog._on_change_date_clicked()
|
||||
|
||||
# Date should not change when cancelled
|
||||
assert dialog._date_iso == original_date
|
||||
|
||||
|
||||
def test_time_log_dialog_change_date_accepted(qtbot, app, fresh_db):
|
||||
"""Test _on_change_date_clicked when user accepts (covers lines 410-450)."""
|
||||
dialog = TimeLogDialog(fresh_db, "2024-01-15", themes=None)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Mock exec to return accepted - the dialog will use whatever date is in the calendar
|
||||
with patch.object(QDialog, "exec", return_value=QDialog.DialogCode.Accepted):
|
||||
# Just verify it doesn't crash - actual date may or may not change
|
||||
# depending on what the real QCalendarWidget selects
|
||||
dialog._on_change_date_clicked()
|
||||
|
||||
# Dialog should still be functional
|
||||
assert dialog._date_iso is not None
|
||||
|
||||
|
||||
def test_time_log_dialog_change_date_with_invalid_current_date(qtbot, app, fresh_db):
|
||||
"""Test _on_change_date_clicked when current date is invalid (covers lines 410-412)."""
|
||||
dialog = TimeLogDialog(fresh_db, "invalid-date", themes=None)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should fall back to current date without crashing
|
||||
with patch.object(QDialog, "exec", return_value=QDialog.DialogCode.Rejected):
|
||||
dialog._on_change_date_clicked()
|
||||
|
||||
|
||||
def test_time_log_dialog_change_date_with_themes(qtbot, app, fresh_db):
|
||||
"""Test _on_change_date_clicked with theme manager (covers line 423-424)."""
|
||||
themes_mock = MagicMock()
|
||||
themes_mock.register_calendar = MagicMock()
|
||||
|
||||
dialog = TimeLogDialog(fresh_db, "2024-01-15", themes=themes_mock)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Mock exec to return rejected
|
||||
with patch.object(QDialog, "exec", return_value=QDialog.DialogCode.Rejected):
|
||||
dialog._on_change_date_clicked()
|
||||
|
||||
# Theme should have been applied to calendar
|
||||
assert themes_mock.register_calendar.called
|
||||
|
||||
|
||||
def test_time_log_dialog_table_item_changed_incomplete_row(qtbot, app, fresh_db):
|
||||
"""Test _on_table_item_changed with incomplete row."""
|
||||
dialog = TimeLogDialog(fresh_db, "2024-01-15", themes=None)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Block signals to prevent Qt cleanup
|
||||
dialog.table.blockSignals(True)
|
||||
|
||||
# Add incomplete row
|
||||
dialog.table.setRowCount(1)
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
|
||||
# Only add project item, missing others
|
||||
proj_item = QTableWidgetItem("Project")
|
||||
dialog.table.setItem(0, 0, proj_item)
|
||||
|
||||
# Call _on_table_item_changed
|
||||
dialog._on_table_item_changed(proj_item)
|
||||
|
||||
dialog.table.blockSignals(False)
|
||||
|
||||
# Should return early without crashing (covers lines 556-558)
|
||||
|
||||
|
||||
def test_time_log_dialog_table_item_changed_creates_new_project(qtbot, app, fresh_db):
|
||||
"""Test _on_table_item_changed creating a new project on the fly."""
|
||||
dialog = TimeLogDialog(fresh_db, "2024-01-15", themes=None)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Block signals to prevent Qt cleanup
|
||||
dialog.table.blockSignals(True)
|
||||
|
||||
# Add a complete row with new project name
|
||||
dialog.table.setRowCount(1)
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
|
||||
proj_item = QTableWidgetItem("Brand New Project")
|
||||
proj_item.setData(Qt.ItemDataRole.UserRole, None) # No entry ID
|
||||
act_item = QTableWidgetItem("Activity")
|
||||
note_item = QTableWidgetItem("Note")
|
||||
hours_item = QTableWidgetItem("2.5")
|
||||
|
||||
dialog.table.setItem(0, 0, proj_item)
|
||||
dialog.table.setItem(0, 1, act_item)
|
||||
dialog.table.setItem(0, 2, note_item)
|
||||
dialog.table.setItem(0, 3, hours_item)
|
||||
|
||||
# Call _on_table_item_changed
|
||||
with patch.object(dialog, "_on_add_or_update"):
|
||||
dialog._on_table_item_changed(proj_item)
|
||||
|
||||
# Should have created project and called add/update
|
||||
projects = fresh_db.list_projects()
|
||||
project_names = [name for _, name in projects]
|
||||
assert "Brand New Project" in project_names
|
||||
|
||||
dialog.table.blockSignals(False)
|
||||
|
||||
|
||||
def test_time_log_dialog_table_item_changed_without_note(qtbot, app, fresh_db):
|
||||
"""Test _on_table_item_changed when note_item is None."""
|
||||
dialog = TimeLogDialog(fresh_db, "2024-01-15", themes=None)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Block signals to prevent Qt cleanup
|
||||
dialog.table.blockSignals(True)
|
||||
|
||||
# Add row without note
|
||||
dialog.table.setRowCount(1)
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
|
||||
proj_item = QTableWidgetItem("Project")
|
||||
proj_item.setData(Qt.ItemDataRole.UserRole, None)
|
||||
act_item = QTableWidgetItem("Activity")
|
||||
hours_item = QTableWidgetItem("1.0")
|
||||
|
||||
dialog.table.setItem(0, 0, proj_item)
|
||||
dialog.table.setItem(0, 1, act_item)
|
||||
# Note: Don't set note_item (leave as None)
|
||||
dialog.table.setItem(0, 3, hours_item)
|
||||
|
||||
# Call _on_table_item_changed
|
||||
with patch.object(dialog, "_on_add_or_update"):
|
||||
dialog._on_table_item_changed(proj_item)
|
||||
|
||||
# Should handle None note gracefully (covers line 567)
|
||||
assert dialog.note.text() == ""
|
||||
|
||||
dialog.table.blockSignals(False)
|
||||
|
||||
|
||||
def test_time_log_dialog_table_item_changed_sets_button_state_for_new_entry(
|
||||
qtbot, app, fresh_db
|
||||
):
|
||||
"""Test that _on_table_item_changed sets correct button state for new entry."""
|
||||
dialog = TimeLogDialog(fresh_db, "2024-01-15", themes=None)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Block signals to prevent Qt cleanup
|
||||
dialog.table.blockSignals(True)
|
||||
|
||||
# Add row without entry ID (new entry)
|
||||
dialog.table.setRowCount(1)
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
|
||||
proj_item = QTableWidgetItem("Project")
|
||||
proj_item.setData(Qt.ItemDataRole.UserRole, None) # No entry ID
|
||||
act_item = QTableWidgetItem("Activity")
|
||||
hours_item = QTableWidgetItem("1.0")
|
||||
|
||||
dialog.table.setItem(0, 0, proj_item)
|
||||
dialog.table.setItem(0, 1, act_item)
|
||||
dialog.table.setItem(0, 3, hours_item)
|
||||
|
||||
with patch.object(dialog, "_on_add_or_update"):
|
||||
dialog._on_table_item_changed(proj_item)
|
||||
|
||||
# Delete button should be disabled for new entry (covers lines 601-603)
|
||||
assert not dialog.delete_btn.isEnabled()
|
||||
|
||||
dialog.table.blockSignals(False)
|
||||
|
||||
|
||||
def test_time_log_dialog_table_item_changed_sets_button_state_for_existing_entry(
|
||||
qtbot, app, fresh_db
|
||||
):
|
||||
"""Test that _on_table_item_changed sets correct button state for existing entry."""
|
||||
# Add a time log entry first
|
||||
proj_id = fresh_db.add_project("Test Project")
|
||||
act_id = fresh_db.add_activity("Activity")
|
||||
entry_id = fresh_db.add_time_log(
|
||||
"2024-01-15", proj_id, act_id, 120, "Note"
|
||||
) # 120 minutes = 2 hours
|
||||
|
||||
dialog = TimeLogDialog(fresh_db, "2024-01-15", themes=None)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Block signals to prevent Qt cleanup
|
||||
dialog.table.blockSignals(True)
|
||||
|
||||
# Add row with entry ID
|
||||
dialog.table.setRowCount(1)
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
|
||||
proj_item = QTableWidgetItem("Test Project")
|
||||
proj_item.setData(Qt.ItemDataRole.UserRole, entry_id)
|
||||
act_item = QTableWidgetItem("Activity")
|
||||
hours_item = QTableWidgetItem("2.0")
|
||||
|
||||
dialog.table.setItem(0, 0, proj_item)
|
||||
dialog.table.setItem(0, 1, act_item)
|
||||
dialog.table.setItem(0, 3, hours_item)
|
||||
|
||||
with patch.object(dialog, "_on_add_or_update"):
|
||||
dialog._on_table_item_changed(proj_item)
|
||||
|
||||
# Delete button should be enabled for existing entry (covers lines 604-606)
|
||||
assert dialog.delete_btn.isEnabled()
|
||||
|
||||
dialog.table.blockSignals(False)
|
||||
|
||||
|
||||
def test_time_log_dialog_table_item_changed_finds_existing_project_by_name(
|
||||
qtbot, app, fresh_db
|
||||
):
|
||||
"""Test _on_table_item_changed finding existing project by name."""
|
||||
proj_id = fresh_db.add_project("Existing Project")
|
||||
|
||||
dialog = TimeLogDialog(fresh_db, "2024-01-15", themes=None)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Block signals to prevent Qt cleanup
|
||||
dialog.table.blockSignals(True)
|
||||
|
||||
# Add row with existing project name
|
||||
dialog.table.setRowCount(1)
|
||||
from PySide6.QtWidgets import QTableWidgetItem
|
||||
|
||||
proj_item = QTableWidgetItem("Existing Project")
|
||||
proj_item.setData(Qt.ItemDataRole.UserRole, None)
|
||||
act_item = QTableWidgetItem("Activity")
|
||||
hours_item = QTableWidgetItem("1.0")
|
||||
|
||||
dialog.table.setItem(0, 0, proj_item)
|
||||
dialog.table.setItem(0, 1, act_item)
|
||||
dialog.table.setItem(0, 3, hours_item)
|
||||
|
||||
with patch.object(dialog, "_on_add_or_update"):
|
||||
dialog._on_table_item_changed(proj_item)
|
||||
|
||||
# Should find and select existing project (covers lines 571-580)
|
||||
assert dialog.project_combo.currentData() == proj_id
|
||||
|
||||
dialog.table.blockSignals(False)
|
||||
|
||||
|
||||
def test_time_report_dialog_initialization(qtbot, app, fresh_db):
|
||||
"""Test TimeReportDialog initialization."""
|
||||
dialog = TimeReportDialog(fresh_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should initialize without crashing
|
||||
assert dialog is not None
|
||||
|
||||
|
||||
def test_time_code_manager_dialog_initialization(qtbot, app, fresh_db):
|
||||
"""Test TimeCodeManagerDialog initialization."""
|
||||
dialog = TimeCodeManagerDialog(fresh_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should initialize without crashing
|
||||
assert dialog is not None
|
||||
|
||||
|
||||
def test_time_code_manager_dialog_with_focus_tab(qtbot, app, fresh_db):
|
||||
"""Test TimeCodeManagerDialog with initial tab focus."""
|
||||
# Test with projects tab
|
||||
dialog = TimeCodeManagerDialog(fresh_db, focus_tab="projects")
|
||||
qtbot.addWidget(dialog)
|
||||
assert dialog.tabs.currentIndex() == 0
|
||||
|
||||
# Test with activities tab
|
||||
dialog2 = TimeCodeManagerDialog(fresh_db, focus_tab="activities")
|
||||
qtbot.addWidget(dialog2)
|
||||
assert dialog2.tabs.currentIndex() == 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue