1060 lines
34 KiB
Python
1060 lines
34 KiB
Python
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from bouquin.db import DBConfig
|
|
from bouquin.documents import DocumentsDialog, TodaysDocumentsWidget
|
|
from PySide6.QtCore import Qt, QUrl
|
|
from PySide6.QtGui import QDesktopServices
|
|
from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox
|
|
|
|
# =============================================================================
|
|
# TodaysDocumentsWidget Tests
|
|
# =============================================================================
|
|
|
|
|
|
def test_todays_documents_widget_init(qtbot, app, fresh_db):
|
|
"""Test TodaysDocumentsWidget initialization."""
|
|
date_iso = "2024-01-15"
|
|
widget = TodaysDocumentsWidget(fresh_db, date_iso)
|
|
qtbot.addWidget(widget)
|
|
|
|
assert widget._db is fresh_db
|
|
assert widget._current_date == date_iso
|
|
assert widget.toggle_btn is not None
|
|
assert widget.open_btn is not None
|
|
assert widget.list is not None
|
|
assert not widget.body.isVisible()
|
|
|
|
|
|
def test_todays_documents_widget_reload_no_documents(qtbot, app, fresh_db):
|
|
"""Test reload when there are no documents for today."""
|
|
date_iso = "2024-01-15"
|
|
widget = TodaysDocumentsWidget(fresh_db, date_iso)
|
|
qtbot.addWidget(widget)
|
|
|
|
# Should have one disabled item saying "no documents"
|
|
assert widget.list.count() == 1
|
|
item = widget.list.item(0)
|
|
assert not (item.flags() & Qt.ItemIsEnabled)
|
|
|
|
|
|
def test_todays_documents_widget_reload_with_documents(qtbot, app, fresh_db):
|
|
"""Test reload when there are documents for today."""
|
|
# Add a project
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
|
|
# Add a document to the project
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test content")
|
|
try:
|
|
fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
# Mark document as accessed today
|
|
date_iso = "2024-01-15"
|
|
# The todays_documents method checks updated_at, so we need to ensure
|
|
# the document shows up in today's query
|
|
|
|
widget = TodaysDocumentsWidget(fresh_db, date_iso)
|
|
qtbot.addWidget(widget)
|
|
|
|
# At minimum, widget should be created without error
|
|
assert widget.list is not None
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_todays_documents_widget_set_current_date(qtbot, app, fresh_db):
|
|
"""Test changing the current date."""
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
|
|
# Change date
|
|
widget.set_current_date("2024-01-16")
|
|
assert widget._current_date == "2024-01-16"
|
|
|
|
|
|
def test_todays_documents_widget_open_document(qtbot, app, fresh_db):
|
|
"""Test opening a document."""
|
|
# 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")
|
|
|
|
try:
|
|
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
|
|
# Mock QDesktopServices.openUrl
|
|
with patch.object(
|
|
QDesktopServices, "openUrl", return_value=True
|
|
) as mock_open_url:
|
|
widget._open_document(doc_id, doc_path.name)
|
|
|
|
# Verify openUrl was called
|
|
assert mock_open_url.called
|
|
args = mock_open_url.call_args[0]
|
|
assert isinstance(args[0], QUrl)
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_todays_documents_widget_open_document_error(qtbot, app, fresh_db):
|
|
"""Test opening a non-existent document shows error."""
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
|
|
# Try to open non-existent document
|
|
with patch.object(QMessageBox, "warning") as mock_warning:
|
|
widget._open_document(99999, "nonexistent.txt")
|
|
assert mock_warning.called
|
|
|
|
|
|
def test_todays_documents_widget_open_documents_dialog(qtbot, app, fresh_db):
|
|
"""Test opening the full documents dialog."""
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
|
|
# Mock DocumentsDialog
|
|
mock_dialog = MagicMock()
|
|
mock_dialog.exec.return_value = QDialog.Accepted
|
|
|
|
with patch("bouquin.documents.DocumentsDialog", return_value=mock_dialog):
|
|
widget._open_documents_dialog()
|
|
assert mock_dialog.exec.called
|
|
|
|
|
|
# =============================================================================
|
|
# DocumentsDialog Tests
|
|
# =============================================================================
|
|
|
|
|
|
def test_documents_dialog_init(qtbot, app, fresh_db):
|
|
"""Test DocumentsDialog initialization."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
assert dialog._db is fresh_db
|
|
assert dialog.project_combo is not None
|
|
assert dialog.search_edit is not None
|
|
assert dialog.table is not None
|
|
assert dialog.table.columnCount() == 5
|
|
|
|
|
|
def test_documents_dialog_init_with_initial_project(qtbot, app, fresh_db):
|
|
"""Test DocumentsDialog with initial project ID."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Should select the specified project
|
|
# Verify project combo is populated
|
|
assert dialog.project_combo.count() > 0
|
|
|
|
|
|
def test_documents_dialog_reload_projects(qtbot, app, fresh_db):
|
|
"""Test reloading projects list."""
|
|
# Add some projects
|
|
fresh_db.add_project("Project 1")
|
|
fresh_db.add_project("Project 2")
|
|
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Check projects are loaded (including "All projects" option)
|
|
assert dialog.project_combo.count() >= 2
|
|
|
|
|
|
def test_documents_dialog_reload_documents_no_project(qtbot, app, fresh_db):
|
|
"""Test reloading documents when no project is selected."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
dialog._reload_documents()
|
|
# Should not crash
|
|
|
|
|
|
def test_documents_dialog_reload_documents_with_project(qtbot, app, fresh_db):
|
|
"""Test reloading documents for a specific project."""
|
|
# Add project and document
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test content")
|
|
|
|
try:
|
|
fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Table should have at least one row
|
|
assert (
|
|
dialog.table.rowCount() >= 0
|
|
) # Might be 0 or 1 depending on how DB is set up
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_add_document(qtbot, app, fresh_db):
|
|
"""Test adding a document."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Create a temporary file to add
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test content")
|
|
|
|
try:
|
|
# Mock file dialog to return our test file
|
|
with patch.object(
|
|
QFileDialog, "getOpenFileNames", return_value=([str(doc_path)], "")
|
|
):
|
|
dialog._on_add_clicked()
|
|
|
|
# Verify document was added (table should reload)
|
|
# The count might not change if the view isn't refreshed properly in test
|
|
# but the DB should have the document
|
|
docs = fresh_db.documents_for_project(proj_id)
|
|
assert len(docs) > 0
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_add_document_no_project(qtbot, app, fresh_db):
|
|
"""Test adding a document with no project selected shows warning."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Set to "All projects" (None)
|
|
dialog.project_combo.setCurrentIndex(0)
|
|
|
|
with patch.object(QMessageBox, "warning") as mock_warning:
|
|
dialog._on_add_clicked()
|
|
assert mock_warning.called
|
|
|
|
|
|
def test_documents_dialog_add_document_file_error(qtbot, app, fresh_db):
|
|
"""Test adding a document that doesn't exist shows warning."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Mock file dialog to return a non-existent file
|
|
with patch.object(
|
|
QFileDialog, "getOpenFileNames", return_value=(["/nonexistent/file.txt"], "")
|
|
):
|
|
with patch.object(QMessageBox, "warning"):
|
|
dialog._on_add_clicked()
|
|
# Should show warning for file not found
|
|
# (this depends on add_document_from_path implementation)
|
|
|
|
|
|
def test_documents_dialog_open_document(qtbot, app, fresh_db):
|
|
"""Test opening a document from the dialog."""
|
|
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))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
with patch.object(
|
|
QDesktopServices, "openUrl", return_value=True
|
|
) as mock_open_url:
|
|
dialog._open_document(doc_id, doc_path.name)
|
|
assert mock_open_url.called
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_delete_document(qtbot, app, fresh_db):
|
|
"""Test deleting a document."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test content")
|
|
|
|
try:
|
|
fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Select the document in the table
|
|
if dialog.table.rowCount() > 0:
|
|
dialog.table.setCurrentCell(0, 0)
|
|
|
|
# Mock confirmation dialog
|
|
with patch.object(
|
|
QMessageBox, "question", return_value=QMessageBox.StandardButton.Yes
|
|
):
|
|
dialog._on_delete_clicked()
|
|
|
|
# Document should be deleted
|
|
fresh_db.documents_for_project(proj_id)
|
|
# Depending on implementation, might be 0 or filtered
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_delete_document_no_selection(qtbot, app, fresh_db):
|
|
"""Test deleting with no selection does nothing."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Don't select anything
|
|
dialog.table.setCurrentCell(-1, -1)
|
|
|
|
# Should not crash
|
|
dialog._on_delete_clicked()
|
|
|
|
|
|
def test_documents_dialog_delete_document_cancelled(qtbot, app, fresh_db):
|
|
"""Test cancelling document deletion."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test content")
|
|
|
|
try:
|
|
fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
if dialog.table.rowCount() > 0:
|
|
dialog.table.setCurrentCell(0, 0)
|
|
|
|
# Mock confirmation dialog to return No
|
|
with patch.object(
|
|
QMessageBox, "question", return_value=QMessageBox.StandardButton.No
|
|
):
|
|
dialog._on_delete_clicked()
|
|
|
|
# Document should still exist
|
|
docs = fresh_db.documents_for_project(proj_id)
|
|
assert len(docs) > 0
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_edit_description(qtbot, app, fresh_db):
|
|
"""Test editing a document's description inline."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test content")
|
|
|
|
try:
|
|
fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
if dialog.table.rowCount() > 0:
|
|
# Get the description cell
|
|
desc_item = dialog.table.item(0, dialog.DESC_COL)
|
|
if desc_item:
|
|
# Simulate editing
|
|
desc_item.setText("New description")
|
|
dialog._on_item_changed(desc_item)
|
|
|
|
# Verify description was updated in DB
|
|
docs = fresh_db.documents_for_project(proj_id)
|
|
if len(docs) > 0:
|
|
_, _, _, _, description, _, _ = docs[0]
|
|
assert description == "New description"
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_edit_tags(qtbot, app, fresh_db):
|
|
"""Test editing a document's tags inline."""
|
|
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))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
if dialog.table.rowCount() > 0:
|
|
# Get the tags cell
|
|
tags_item = dialog.table.item(0, dialog.TAGS_COL)
|
|
if tags_item:
|
|
# Simulate editing tags
|
|
tags_item.setText("tag1, tag2, tag3")
|
|
dialog._on_item_changed(tags_item)
|
|
|
|
# Verify tags were updated in DB
|
|
tags = fresh_db.get_tags_for_document(doc_id)
|
|
tag_names = [name for (_, name, _) in tags]
|
|
assert "tag1" in tag_names
|
|
assert "tag2" in tag_names
|
|
assert "tag3" in tag_names
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_tags_color_application(qtbot, app, fresh_db):
|
|
"""Test that tag colors are applied to the tags cell."""
|
|
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))
|
|
|
|
# Add a tag with a color
|
|
fresh_db.add_tag("colored_tag", "#FF0000")
|
|
fresh_db.set_tags_for_document(doc_id, ["colored_tag"])
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
if dialog.table.rowCount() > 0:
|
|
tags_item = dialog.table.item(0, dialog.TAGS_COL)
|
|
if tags_item:
|
|
# Check that background color was applied
|
|
bg_color = tags_item.background().color()
|
|
assert bg_color.isValid()
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_search_functionality(qtbot, app, fresh_db):
|
|
"""Test search functionality across all projects."""
|
|
# Add multiple projects with documents
|
|
proj1 = fresh_db.add_project("Project 1")
|
|
proj2 = fresh_db.add_project("Project 2")
|
|
|
|
doc1_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc1_path.write_text("apple content")
|
|
doc2_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc2_path.write_text("banana content")
|
|
|
|
try:
|
|
fresh_db.add_document_from_path(proj1, str(doc1_path))
|
|
fresh_db.add_document_from_path(proj2, str(doc2_path))
|
|
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Perform search
|
|
dialog.search_edit.setText("apple")
|
|
dialog._on_search_text_changed("apple")
|
|
|
|
# Should show search results
|
|
# Implementation depends on search_documents query
|
|
finally:
|
|
doc1_path.unlink(missing_ok=True)
|
|
doc2_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_manage_projects_button(qtbot, app, fresh_db):
|
|
"""Test clicking manage projects button."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Mock TimeCodeManagerDialog
|
|
mock_mgr_dialog = MagicMock()
|
|
mock_mgr_dialog.exec.return_value = QDialog.Accepted
|
|
|
|
with patch("bouquin.documents.TimeCodeManagerDialog", return_value=mock_mgr_dialog):
|
|
dialog._manage_projects()
|
|
assert mock_mgr_dialog.exec.called
|
|
|
|
|
|
def test_documents_dialog_format_size(qtbot, app, fresh_db):
|
|
"""Test file size formatting."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Test various sizes
|
|
assert "B" in dialog._format_size(500)
|
|
assert "KB" in dialog._format_size(2048)
|
|
assert "MB" in dialog._format_size(2 * 1024 * 1024)
|
|
assert "GB" in dialog._format_size(2 * 1024 * 1024 * 1024)
|
|
|
|
|
|
def test_documents_dialog_current_project_all(qtbot, app, fresh_db):
|
|
"""Test _current_project returns None for 'All Projects'."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Set to first item (All Projects)
|
|
dialog.project_combo.setCurrentIndex(0)
|
|
|
|
proj_id = dialog._current_project()
|
|
assert proj_id is None
|
|
|
|
|
|
def test_documents_dialog_current_project_specific(qtbot, app, fresh_db):
|
|
"""Test _current_project returns correct project ID."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Find and select the test project
|
|
for i in range(dialog.project_combo.count()):
|
|
if dialog.project_combo.itemData(i) == proj_id:
|
|
dialog.project_combo.setCurrentIndex(i)
|
|
break
|
|
|
|
current_proj = dialog._current_project()
|
|
if current_proj is not None:
|
|
assert current_proj == proj_id
|
|
|
|
|
|
def test_documents_dialog_table_double_click_opens_document(qtbot, app, fresh_db):
|
|
"""Test double-clicking a document opens it."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test content")
|
|
|
|
try:
|
|
fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
if dialog.table.rowCount() > 0:
|
|
with patch.object(QDesktopServices, "openUrl", return_value=True):
|
|
# Simulate double-click
|
|
dialog._on_open_clicked()
|
|
|
|
# Should attempt to open if a row is selected
|
|
# (behavior depends on whether table selection is set up properly)
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_selected_doc_meta_no_selection(qtbot, app, fresh_db):
|
|
"""Test _selected_doc_meta with no selection."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
doc_id, file_name = dialog._selected_doc_meta()
|
|
assert doc_id is None
|
|
assert file_name is None
|
|
|
|
|
|
def test_documents_dialog_selected_doc_meta_with_selection(qtbot, app, fresh_db):
|
|
"""Test _selected_doc_meta with a valid selection."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test content")
|
|
|
|
try:
|
|
fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
if dialog.table.rowCount() > 0:
|
|
dialog.table.setCurrentCell(0, 0)
|
|
|
|
sel_doc_id, sel_file_name = dialog._selected_doc_meta()
|
|
# May or may not be None depending on how table is populated
|
|
# At minimum, should not crash
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_item_changed_ignores_during_reload(qtbot, app, fresh_db):
|
|
"""Test _on_item_changed is ignored during reload."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Set reloading flag
|
|
dialog._reloading_docs = True
|
|
|
|
# Create a mock item
|
|
from PySide6.QtWidgets import QTableWidgetItem
|
|
|
|
item = QTableWidgetItem("test")
|
|
|
|
# Should not crash or do anything
|
|
dialog._on_item_changed(item)
|
|
|
|
dialog._reloading_docs = False
|
|
|
|
|
|
def test_documents_dialog_search_clears_properly(qtbot, app, fresh_db):
|
|
"""Test clearing search box resets to project view."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Enter search text
|
|
dialog.search_edit.setText("test")
|
|
dialog._on_search_text_changed("test")
|
|
|
|
# Clear search
|
|
dialog.search_edit.clear()
|
|
dialog._on_search_text_changed("")
|
|
|
|
# Should reset to normal project view
|
|
assert dialog._search_text == ""
|
|
|
|
|
|
def test_todays_documents_widget_reload_with_project_names(qtbot, app, fresh_db):
|
|
"""Test reload when documents have project names."""
|
|
# Add a project and document
|
|
proj_id = fresh_db.add_project("My 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 todays_documents to return a document with project name
|
|
with patch.object(fresh_db, "todays_documents") as mock_today:
|
|
mock_today.return_value = [(doc_id, doc_path.name, "My Project")]
|
|
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
widget.reload()
|
|
|
|
# Should have one item with project name in label
|
|
assert widget.list.count() == 1
|
|
item = widget.list.item(0)
|
|
assert "My Project" in item.text()
|
|
assert doc_path.name in item.text()
|
|
|
|
# Check data was stored
|
|
data = item.data(Qt.ItemDataRole.UserRole)
|
|
assert isinstance(data, dict)
|
|
assert data["doc_id"] == doc_id
|
|
assert data["file_name"] == doc_path.name
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_todays_documents_widget_on_toggle_expand(qtbot, app, fresh_db):
|
|
"""Test toggle behavior when expanding."""
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
widget.show()
|
|
qtbot.waitExposed(widget)
|
|
|
|
# Initially collapsed
|
|
assert not widget.body.isVisible()
|
|
|
|
# Call _on_toggle directly
|
|
widget._on_toggle(True)
|
|
|
|
# Should be expanded
|
|
assert widget.body.isVisible()
|
|
assert widget.toggle_btn.arrowType() == Qt.DownArrow
|
|
|
|
|
|
def test_todays_documents_widget_on_toggle_collapse(qtbot, app, fresh_db):
|
|
"""Test toggle behavior when collapsing."""
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
widget.show()
|
|
qtbot.waitExposed(widget)
|
|
|
|
# Expand first
|
|
widget._on_toggle(True)
|
|
assert widget.body.isVisible()
|
|
|
|
# Now collapse
|
|
widget._on_toggle(False)
|
|
|
|
# Should be collapsed
|
|
assert not widget.body.isVisible()
|
|
assert widget.toggle_btn.arrowType() == Qt.RightArrow
|
|
|
|
|
|
def test_todays_documents_widget_set_current_date_triggers_reload(qtbot, app, fresh_db):
|
|
"""Test that set_current_date triggers a reload."""
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
|
|
# Mock reload to verify it's called
|
|
with patch.object(widget, "reload") as mock_reload:
|
|
widget.set_current_date("2024-01-16")
|
|
|
|
assert widget._current_date == "2024-01-16"
|
|
assert mock_reload.called
|
|
|
|
|
|
def test_todays_documents_widget_double_click_with_invalid_data(qtbot, app, fresh_db):
|
|
"""Test double-clicking item with invalid data."""
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
|
|
# Add item with invalid data
|
|
from PySide6.QtWidgets import QListWidgetItem
|
|
|
|
item = QListWidgetItem("Test")
|
|
item.setData(Qt.ItemDataRole.UserRole, "not a dict")
|
|
widget.list.addItem(item)
|
|
|
|
# Double-click should not crash
|
|
widget._open_selected_document(item)
|
|
|
|
|
|
def test_todays_documents_widget_double_click_with_missing_doc_id(qtbot, app, fresh_db):
|
|
"""Test double-clicking item with missing doc_id."""
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
|
|
from PySide6.QtWidgets import QListWidgetItem
|
|
|
|
item = QListWidgetItem("Test")
|
|
item.setData(Qt.ItemDataRole.UserRole, {"file_name": "test.txt"})
|
|
widget.list.addItem(item)
|
|
|
|
# Should return early without crashing
|
|
widget._open_selected_document(item)
|
|
|
|
|
|
def test_todays_documents_widget_double_click_with_missing_filename(
|
|
qtbot, app, fresh_db
|
|
):
|
|
"""Test double-clicking item with missing file_name."""
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
|
|
from PySide6.QtWidgets import QListWidgetItem
|
|
|
|
item = QListWidgetItem("Test")
|
|
item.setData(Qt.ItemDataRole.UserRole, {"doc_id": 1})
|
|
widget.list.addItem(item)
|
|
|
|
# Should return early without crashing
|
|
widget._open_selected_document(item)
|
|
|
|
|
|
def test_documents_dialog_reload_calls_on_init(qtbot, app, fresh_db):
|
|
"""Test that _reload_documents is called on initialization."""
|
|
# Add a project so the combo will have items
|
|
fresh_db.add_project("Test Project")
|
|
|
|
# This covers line 300
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Should have projects loaded (covers _reload_projects line 300-301)
|
|
assert dialog.project_combo.count() > 0
|
|
|
|
|
|
def test_documents_dialog_tags_column_hidden_when_disabled(qtbot, app, tmp_path):
|
|
"""Test that tags column is hidden when tags are disabled in config."""
|
|
# Create a config with tags disabled
|
|
db_path = tmp_path / "test.db"
|
|
cfg = DBConfig(
|
|
path=db_path,
|
|
key="test-key",
|
|
idle_minutes=0,
|
|
theme="light",
|
|
move_todos=True,
|
|
tags=False, # Tags disabled
|
|
time_log=True,
|
|
reminders=True,
|
|
locale="en",
|
|
font_size=11,
|
|
)
|
|
|
|
from bouquin.db import DBManager
|
|
|
|
db = DBManager(cfg)
|
|
db.connect()
|
|
|
|
try:
|
|
# Add project and document
|
|
proj_id = db.add_project("Test Project")
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test")
|
|
|
|
try:
|
|
db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
# Patch load_db_config to return our custom config
|
|
with patch("bouquin.documents.load_db_config", return_value=cfg):
|
|
dialog = DocumentsDialog(db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Tags column should be hidden (covers lines 400-401)
|
|
# The column is hidden inside _reload_documents when there are rows
|
|
assert dialog.table.isColumnHidden(dialog.TAGS_COL)
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_documents_dialog_project_changed_triggers_reload(qtbot, app, fresh_db):
|
|
"""Test that changing project triggers document reload."""
|
|
fresh_db.add_project("Project 1")
|
|
fresh_db.add_project("Project 2")
|
|
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Mock _reload_documents
|
|
with patch.object(dialog, "_reload_documents") as mock_reload:
|
|
# Change project
|
|
dialog._on_project_changed(1)
|
|
|
|
# Should have triggered reload (covers line 421-424)
|
|
assert mock_reload.called
|
|
|
|
|
|
def test_documents_dialog_add_with_cancelled_dialog(qtbot, app, fresh_db):
|
|
"""Test adding document when file dialog is cancelled."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Mock file dialog to return empty (cancelled)
|
|
with patch.object(QFileDialog, "getOpenFileNames", return_value=([], "")):
|
|
initial_count = dialog.table.rowCount()
|
|
dialog._on_add_clicked()
|
|
|
|
# No documents should be added (covers line 442)
|
|
assert dialog.table.rowCount() == initial_count
|
|
|
|
|
|
def test_documents_dialog_delete_with_cancelled_confirmation(qtbot, app, fresh_db):
|
|
"""Test deleting document when user cancels confirmation."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test")
|
|
|
|
try:
|
|
fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
if dialog.table.rowCount() > 0:
|
|
dialog.table.setCurrentCell(0, 0)
|
|
|
|
# Mock to return No
|
|
with patch.object(
|
|
QMessageBox, "question", return_value=QMessageBox.StandardButton.No
|
|
):
|
|
dialog._on_delete_clicked()
|
|
|
|
# Document should still exist (covers line 486)
|
|
docs = fresh_db.documents_for_project(proj_id)
|
|
assert len(docs) > 0
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_edit_tags_with_empty_result(qtbot, app, fresh_db):
|
|
"""Test editing tags when result is empty after setting."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test")
|
|
|
|
try:
|
|
fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
if dialog.table.rowCount() > 0:
|
|
tags_item = dialog.table.item(0, dialog.TAGS_COL)
|
|
if tags_item:
|
|
# Set empty tags
|
|
tags_item.setText("")
|
|
dialog._on_item_changed(tags_item)
|
|
|
|
# Background should be cleared (covers lines 523-524)
|
|
# Just verify no crash
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_on_open_with_no_selection(qtbot, app, fresh_db):
|
|
"""Test _on_open_clicked with no selection."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Don't select anything
|
|
dialog.table.setCurrentCell(-1, -1)
|
|
|
|
# Should not crash (early return)
|
|
dialog._on_open_clicked()
|
|
|
|
|
|
def test_documents_dialog_search_with_results(qtbot, app, fresh_db):
|
|
"""Test search functionality with actual results."""
|
|
proj_id = fresh_db.add_project("Test Project")
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("searchable content")
|
|
|
|
try:
|
|
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
# Update document to have searchable description
|
|
fresh_db.update_document_description(doc_id, "searchable description")
|
|
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Mock search_documents to return results
|
|
with patch.object(fresh_db, "search_documents") as mock_search:
|
|
mock_search.return_value = [
|
|
(
|
|
doc_id,
|
|
proj_id,
|
|
"Test Project",
|
|
doc_path.name,
|
|
"searchable description",
|
|
100,
|
|
"2024-01-15",
|
|
)
|
|
]
|
|
|
|
# Perform search
|
|
dialog.search_edit.setText("searchable")
|
|
dialog._on_search_text_changed("searchable")
|
|
|
|
# Should show results
|
|
assert dialog.table.rowCount() > 0
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_documents_dialog_on_item_changed_invalid_item(qtbot, app, fresh_db):
|
|
"""Test _on_item_changed with None item."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Call with None
|
|
dialog._on_item_changed(None)
|
|
|
|
# Should not crash
|
|
|
|
|
|
def test_documents_dialog_on_item_changed_no_file_item(qtbot, app, fresh_db):
|
|
"""Test _on_item_changed when file item is None."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Manually add a row without proper file item
|
|
dialog.table.setRowCount(1)
|
|
from PySide6.QtWidgets import QTableWidgetItem
|
|
|
|
desc_item = QTableWidgetItem("Test")
|
|
dialog.table.setItem(0, dialog.DESC_COL, desc_item)
|
|
|
|
# Call on_item_changed
|
|
dialog._on_item_changed(desc_item)
|
|
|
|
# Should return early without crashing
|
|
|
|
|
|
def test_documents_dialog_format_size_edge_cases(qtbot, app, fresh_db):
|
|
"""Test _format_size with edge cases."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Test 0 bytes
|
|
assert dialog._format_size(0) == "0 B"
|
|
|
|
# Test exact KB boundary
|
|
assert "1.0 KB" in dialog._format_size(1024)
|
|
|
|
# Test exact MB boundary
|
|
assert "1.0 MB" in dialog._format_size(1024 * 1024)
|
|
|
|
# Test exact GB boundary
|
|
assert "1.0 GB" in dialog._format_size(1024 * 1024 * 1024)
|
|
|
|
|
|
def test_documents_dialog_selected_doc_meta_no_file_item(qtbot, app, fresh_db):
|
|
"""Test _selected_doc_meta when file item is None."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Add a row without file item
|
|
dialog.table.setRowCount(1)
|
|
dialog.table.setCurrentCell(0, 0)
|
|
|
|
doc_id, file_name = dialog._selected_doc_meta()
|
|
|
|
# Should return None, None
|
|
assert doc_id is None
|
|
assert file_name is None
|
|
|
|
|
|
def test_documents_dialog_initial_project_selection(qtbot, app, fresh_db):
|
|
"""Test dialog with initial_project_id selects correct project."""
|
|
proj_id = fresh_db.add_project("Selected Project")
|
|
|
|
# Add a document to ensure something shows
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text("test")
|
|
|
|
try:
|
|
fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
|
|
dialog = DocumentsDialog(fresh_db, initial_project_id=proj_id)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Should have selected the project
|
|
current_proj = dialog._current_project()
|
|
assert current_proj == proj_id
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
|
|
def test_todays_documents_widget_reload_multiple_documents(qtbot, app, fresh_db):
|
|
"""Test reload with multiple documents."""
|
|
proj_id = fresh_db.add_project("Project")
|
|
|
|
# Add multiple documents
|
|
doc_ids = []
|
|
for i in range(3):
|
|
doc_path = Path(tempfile.mktemp(suffix=".txt"))
|
|
doc_path.write_text(f"content {i}")
|
|
try:
|
|
doc_id = fresh_db.add_document_from_path(proj_id, str(doc_path))
|
|
doc_ids.append((doc_id, doc_path.name))
|
|
finally:
|
|
doc_path.unlink(missing_ok=True)
|
|
|
|
# Mock todays_documents
|
|
with patch.object(fresh_db, "todays_documents") as mock_today:
|
|
mock_today.return_value = [
|
|
(doc_id, name, "Project") for doc_id, name in doc_ids
|
|
]
|
|
|
|
widget = TodaysDocumentsWidget(fresh_db, "2024-01-15")
|
|
qtbot.addWidget(widget)
|
|
widget.reload()
|
|
|
|
# Should have 3 items
|
|
assert widget.list.count() == 3
|
|
|
|
|
|
def test_documents_dialog_manage_projects_button_clicked(qtbot, app, fresh_db):
|
|
"""Test clicking manage projects button."""
|
|
dialog = DocumentsDialog(fresh_db)
|
|
qtbot.addWidget(dialog)
|
|
|
|
# Mock TimeCodeManagerDialog
|
|
mock_mgr_dialog = MagicMock()
|
|
mock_mgr_dialog.exec.return_value = QDialog.Accepted
|
|
|
|
with patch("bouquin.documents.TimeCodeManagerDialog", return_value=mock_mgr_dialog):
|
|
dialog._manage_projects()
|
|
|
|
# Should have opened the manager dialog
|
|
assert mock_mgr_dialog.exec.called
|