additional test coverage and mypy fixes
All checks were successful
Python CI / test (push) Successful in 1m18s
Lint / lint (push) Successful in 1m5s
Tests / test (3.11) (push) Successful in 1m29s
Tests / test (3.12) (push) Successful in 1m33s
Tests / test (3.13) (push) Successful in 1m26s
Tests / test (3.14) (push) Successful in 1m14s
All checks were successful
Python CI / test (push) Successful in 1m18s
Lint / lint (push) Successful in 1m5s
Tests / test (3.11) (push) Successful in 1m29s
Tests / test (3.12) (push) Successful in 1m33s
Tests / test (3.13) (push) Successful in 1m26s
Tests / test (3.14) (push) Successful in 1m14s
This commit is contained in:
parent
f6ed11b0bc
commit
558b7be01a
@ -14,7 +14,8 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, Dict, Any, Tuple
|
||||
from typing import Optional, Callable, Dict, Any, Tuple, Union
|
||||
from concurrent.futures import Future
|
||||
from collections import OrderedDict
|
||||
import threading
|
||||
|
||||
@ -230,7 +231,7 @@ class AsyncImageLoader(QObject):
|
||||
self.executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="ImageLoader")
|
||||
|
||||
# Priority queue and tracking
|
||||
self._queue: asyncio.PriorityQueue = None # Created when event loop starts
|
||||
self._queue: Optional[asyncio.PriorityQueue[Any]] = None # Created when event loop starts
|
||||
self._pending_requests: Dict[Path, LoadRequest] = {}
|
||||
self._active_tasks: Dict[Path, asyncio.Task] = {}
|
||||
self._next_request_id = 0
|
||||
@ -373,7 +374,7 @@ class AsyncImageLoader(QObject):
|
||||
self._pending_requests.pop(path, None)
|
||||
self._active_tasks.pop(path, None)
|
||||
|
||||
def _load_and_process_image(self, path: Path, target_size: Optional[Tuple[int, int]]) -> Image.Image:
|
||||
def _load_and_process_image(self, path: Path, target_size: Optional[Tuple[int, int]]) -> "Image.Image":
|
||||
"""
|
||||
Load image from disk and process (runs in thread pool).
|
||||
|
||||
@ -528,7 +529,7 @@ class AsyncPDFGenerator(QObject):
|
||||
self.executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="PDFGenerator")
|
||||
|
||||
# Export state
|
||||
self._current_export: Optional[asyncio.Task] = None
|
||||
self._current_export: Optional[Future[Any]] = None
|
||||
self._cancel_requested = False
|
||||
self._lock = threading.Lock()
|
||||
self._shutdown = False
|
||||
@ -675,7 +676,7 @@ class AsyncPDFGenerator(QObject):
|
||||
with self._lock:
|
||||
self._current_export = None
|
||||
|
||||
def _export_with_cache(self, exporter, output_path: str, progress_callback) -> Tuple[bool, list]:
|
||||
def _export_with_cache(self, exporter: Any, output_path: str, progress_callback: Any) -> Tuple[bool, list[Any]]:
|
||||
"""
|
||||
Run PDF export with image cache integration.
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Tuple
|
||||
from typing import Dict, Optional, List, Tuple
|
||||
from pyPhotoAlbum.project_serializer import save_to_zip, load_from_zip
|
||||
|
||||
|
||||
@ -186,7 +186,7 @@ class AutosaveManager:
|
||||
max_count: Maximum number of checkpoints to keep per project
|
||||
"""
|
||||
now = datetime.now()
|
||||
checkpoints_by_project = {}
|
||||
checkpoints_by_project: Dict[str, List[Tuple[Path, dict]]] = {}
|
||||
|
||||
# Group checkpoints by project
|
||||
for checkpoint_path, metadata in self.list_checkpoints():
|
||||
|
||||
@ -98,6 +98,7 @@ class AddElementCommand(Command):
|
||||
elem_data = data["element"]
|
||||
elem_type = elem_data.get("type")
|
||||
|
||||
element: BaseLayoutElement
|
||||
if elem_type == "image":
|
||||
element = ImageData()
|
||||
elif elem_type == "placeholder":
|
||||
@ -156,6 +157,7 @@ class DeleteElementCommand(Command):
|
||||
elem_data = data["element"]
|
||||
elem_type = elem_data.get("type")
|
||||
|
||||
element: BaseLayoutElement
|
||||
if elem_type == "image":
|
||||
element = ImageData()
|
||||
elif elem_type == "placeholder":
|
||||
@ -207,6 +209,7 @@ class MoveElementCommand(Command):
|
||||
elem_data = data["element"]
|
||||
elem_type = elem_data.get("type")
|
||||
|
||||
element: BaseLayoutElement
|
||||
if elem_type == "image":
|
||||
element = ImageData()
|
||||
elif elem_type == "placeholder":
|
||||
@ -264,6 +267,7 @@ class ResizeElementCommand(Command):
|
||||
elem_data = data["element"]
|
||||
elem_type = elem_data.get("type")
|
||||
|
||||
element: BaseLayoutElement
|
||||
if elem_type == "image":
|
||||
element = ImageData()
|
||||
elif elem_type == "placeholder":
|
||||
@ -388,6 +392,7 @@ class RotateElementCommand(Command):
|
||||
elem_data = data["element"]
|
||||
elem_type = elem_data.get("type")
|
||||
|
||||
element: BaseLayoutElement
|
||||
if elem_type == "image":
|
||||
element = ImageData()
|
||||
elif elem_type == "placeholder":
|
||||
@ -485,6 +490,7 @@ class AlignElementsCommand(Command):
|
||||
elem_data = change_data["element"]
|
||||
elem_type = elem_data.get("type")
|
||||
|
||||
element: BaseLayoutElement
|
||||
if elem_type == "image":
|
||||
element = ImageData()
|
||||
elif elem_type == "placeholder":
|
||||
@ -546,6 +552,7 @@ class ResizeElementsCommand(Command):
|
||||
elem_data = change_data["element"]
|
||||
elem_type = elem_data.get("type")
|
||||
|
||||
element: BaseLayoutElement
|
||||
if elem_type == "image":
|
||||
element = ImageData()
|
||||
elif elem_type == "placeholder":
|
||||
@ -605,6 +612,7 @@ class ChangeZOrderCommand(Command):
|
||||
elem_data = data["element"]
|
||||
elem_type = elem_data.get("type")
|
||||
|
||||
element: BaseLayoutElement
|
||||
if elem_type == "image":
|
||||
element = ImageData()
|
||||
elif elem_type == "placeholder":
|
||||
|
||||
@ -4,7 +4,7 @@ Decorator system for pyPhotoAlbum ribbon UI
|
||||
|
||||
import copy
|
||||
from functools import wraps
|
||||
from typing import Optional, Callable
|
||||
from typing import Any, Optional, Callable
|
||||
|
||||
|
||||
class RibbonAction:
|
||||
@ -75,11 +75,11 @@ class RibbonAction:
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# Store metadata on wrapper function
|
||||
wrapper._ribbon_action = {
|
||||
wrapper._ribbon_action = { # type: ignore[attr-defined]
|
||||
"label": self.label,
|
||||
"tooltip": self.tooltip,
|
||||
"tab": self.tab,
|
||||
@ -178,11 +178,11 @@ class NumericalInput:
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# Store metadata on wrapper function
|
||||
wrapper._numerical_input = {"fields": self.fields}
|
||||
wrapper._numerical_input = {"fields": self.fields} # type: ignore[attr-defined]
|
||||
|
||||
return wrapper
|
||||
|
||||
@ -218,7 +218,7 @@ class UndoableOperation:
|
||||
# Decorator handles undo/redo automatically
|
||||
"""
|
||||
|
||||
def __init__(self, capture: str = "page_elements", description: str = None):
|
||||
def __init__(self, capture: str = "page_elements", description: Optional[str] = None):
|
||||
"""
|
||||
Initialize the undoable operation decorator.
|
||||
|
||||
@ -298,10 +298,11 @@ class UndoableOperation:
|
||||
current_page.layout.elements.clear()
|
||||
|
||||
# Restore elements from serialized state
|
||||
from pyPhotoAlbum.models import ImageData, PlaceholderData, TextBoxData
|
||||
from pyPhotoAlbum.models import BaseLayoutElement, ImageData, PlaceholderData, TextBoxData
|
||||
|
||||
for elem_data in state:
|
||||
elem_type = elem_data.get("type")
|
||||
elem: BaseLayoutElement
|
||||
if elem_type == "image":
|
||||
elem = ImageData()
|
||||
elif elem_type == "placeholder":
|
||||
@ -315,7 +316,7 @@ class UndoableOperation:
|
||||
current_page.layout.add_element(elem)
|
||||
|
||||
|
||||
def undoable_operation(capture: str = "page_elements", description: str = None) -> Callable:
|
||||
def undoable_operation(capture: str = "page_elements", description: Optional[str] = None) -> Callable:
|
||||
"""
|
||||
Convenience function for the UndoableOperation decorator.
|
||||
|
||||
|
||||
@ -30,11 +30,11 @@ def apply_pil_rotation(image: Image.Image, pil_rotation_90: int) -> Image.Image:
|
||||
|
||||
angle = pil_rotation_90 * 90
|
||||
if angle == 90:
|
||||
return image.transpose(Image.ROTATE_270) # CCW 90 = rotate right
|
||||
return image.transpose(Image.Transpose.ROTATE_270) # CCW 90 = rotate right
|
||||
elif angle == 180:
|
||||
return image.transpose(Image.ROTATE_180)
|
||||
return image.transpose(Image.Transpose.ROTATE_180)
|
||||
elif angle == 270:
|
||||
return image.transpose(Image.ROTATE_90) # CCW 270 = rotate left
|
||||
return image.transpose(Image.Transpose.ROTATE_90) # CCW 270 = rotate left
|
||||
|
||||
return image
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ Displays loading progress in the lower-right corner of the window.
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QProgressBar
|
||||
from PyQt6.QtCore import Qt, QPropertyAnimation, QEasingCurve, pyqtProperty
|
||||
from PyQt6.QtCore import Qt, QPropertyAnimation, QEasingCurve, pyqtProperty # type: ignore[attr-defined]
|
||||
from PyQt6.QtGui import QPalette, QColor
|
||||
|
||||
|
||||
@ -98,8 +98,8 @@ class LoadingWidget(QWidget):
|
||||
"""Get opacity for animation"""
|
||||
return self._opacity
|
||||
|
||||
@opacity.setter
|
||||
def opacity(self, value):
|
||||
@opacity.setter # type: ignore[no-redef]
|
||||
def opacity(self, value: float) -> None:
|
||||
"""Set opacity for animation"""
|
||||
self._opacity = value
|
||||
self.setWindowOpacity(value)
|
||||
|
||||
@ -79,7 +79,7 @@ class MergeManager:
|
||||
print("MergeManager: One or both projects lack project_id, assuming concatenation")
|
||||
return False
|
||||
|
||||
return project_a_id == project_b_id
|
||||
return bool(project_a_id == project_b_id)
|
||||
|
||||
def detect_conflicts(
|
||||
self, our_project_data: Dict[str, Any], their_project_data: Dict[str, Any]
|
||||
@ -303,7 +303,7 @@ class MergeManager:
|
||||
# Note: If timestamps differ, we assume one version modified it and the other didn't.
|
||||
# The _merge_non_conflicting_changes method will automatically use the newer version.
|
||||
|
||||
def auto_resolve_conflicts(self, strategy: MergeStrategy = MergeStrategy.LATEST_WINS) -> Dict[str, Any]:
|
||||
def auto_resolve_conflicts(self, strategy: MergeStrategy = MergeStrategy.LATEST_WINS) -> Dict[int, str]:
|
||||
"""
|
||||
Automatically resolve conflicts based on a strategy.
|
||||
|
||||
|
||||
@ -4,17 +4,31 @@ Async loading mixin for non-blocking image loading and PDF generation.
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
import logging
|
||||
|
||||
from PyQt6.QtCore import QObject
|
||||
from PyQt6.QtWidgets import QProgressDialog
|
||||
|
||||
from pyPhotoAlbum.async_backend import AsyncImageLoader, AsyncPDFGenerator, ImageCache, LoadPriority
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt6.QtWidgets import QMainWindow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncLoadingMixin:
|
||||
# Type hints for expected attributes from mixing class
|
||||
_pdf_progress_dialog: Optional[QProgressDialog]
|
||||
|
||||
def update(self) -> None:
|
||||
"""Expected from QWidget"""
|
||||
...
|
||||
|
||||
def window(self) -> "QMainWindow":
|
||||
"""Expected from QWidget"""
|
||||
...
|
||||
"""
|
||||
Mixin to add async loading capabilities to GLWidget.
|
||||
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
Base mixin providing shared application state access
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from PyQt6.QtWidgets import QStatusBar, QMessageBox
|
||||
from typing import Any, Optional, cast
|
||||
from PyQt6.QtWidgets import QStatusBar, QMessageBox, QWidget
|
||||
|
||||
|
||||
class ApplicationStateMixin:
|
||||
@ -44,7 +44,7 @@ class ApplicationStateMixin:
|
||||
"""Access to status bar"""
|
||||
if not hasattr(self, "_status_bar"):
|
||||
raise AttributeError("MainWindow must set _status_bar attribute")
|
||||
return self._status_bar
|
||||
return cast(QStatusBar, self._status_bar)
|
||||
|
||||
@property
|
||||
def template_manager(self):
|
||||
@ -117,7 +117,7 @@ class ApplicationStateMixin:
|
||||
"""
|
||||
if not self.project or not self.project.pages:
|
||||
return -1
|
||||
return self.gl_widget.current_page_index
|
||||
return int(self.gl_widget.current_page_index)
|
||||
|
||||
def show_status(self, message: str, timeout: int = 2000):
|
||||
"""
|
||||
@ -138,7 +138,7 @@ class ApplicationStateMixin:
|
||||
title: Dialog title
|
||||
message: Error message
|
||||
"""
|
||||
QMessageBox.critical(self, title, message)
|
||||
QMessageBox.critical(cast(QWidget, self), title, message)
|
||||
|
||||
def show_warning(self, title: str, message: str):
|
||||
"""
|
||||
@ -148,7 +148,7 @@ class ApplicationStateMixin:
|
||||
title: Dialog title
|
||||
message: Warning message
|
||||
"""
|
||||
QMessageBox.warning(self, title, message)
|
||||
QMessageBox.warning(cast(QWidget, self), title, message)
|
||||
|
||||
def show_info(self, title: str, message: str):
|
||||
"""
|
||||
@ -158,7 +158,7 @@ class ApplicationStateMixin:
|
||||
title: Dialog title
|
||||
message: Information message
|
||||
"""
|
||||
QMessageBox.information(self, title, message)
|
||||
QMessageBox.information(cast(QWidget, self), title, message)
|
||||
|
||||
def require_page(self, show_warning: bool = True) -> bool:
|
||||
"""
|
||||
|
||||
@ -2,18 +2,31 @@
|
||||
Element manipulation mixin for GLWidget - handles element transformations
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Any, Optional, Tuple
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyPhotoAlbum.models import BaseLayoutElement
|
||||
from PyQt6.QtWidgets import QMainWindow
|
||||
|
||||
|
||||
class ElementManipulationMixin:
|
||||
"""
|
||||
Mixin providing element transformation functionality.
|
||||
# Type hints for expected attributes from mixing class
|
||||
selected_element: Optional["BaseLayoutElement"]
|
||||
drag_start_pos: Optional[Tuple[float, float]]
|
||||
drag_start_element_pos: Optional[Tuple[float, float]]
|
||||
|
||||
This mixin handles resizing, rotating, and moving elements, including
|
||||
snapping support and cross-page element transfers.
|
||||
"""
|
||||
def window(self) -> "QMainWindow":
|
||||
"""Expected from QWidget"""
|
||||
...
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initialize element manipulation mixin.
|
||||
|
||||
This mixin provides element transformation functionality including
|
||||
resizing, rotating, moving elements, snapping support and cross-page
|
||||
element transfers.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Resize state
|
||||
|
||||
@ -2,11 +2,21 @@
|
||||
Element selection mixin for GLWidget - handles element selection and hit detection
|
||||
"""
|
||||
|
||||
from typing import Optional, Set
|
||||
from typing import Any, TYPE_CHECKING, Optional, Set
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt6.QtWidgets import QMainWindow
|
||||
|
||||
from pyPhotoAlbum.models import BaseLayoutElement
|
||||
|
||||
|
||||
class ElementSelectionMixin:
|
||||
# Type hints for expected attributes from mixing class
|
||||
_page_renderers: list
|
||||
|
||||
def window(self) -> "QMainWindow":
|
||||
"""Expected from QWidget"""
|
||||
...
|
||||
"""
|
||||
Mixin providing element selection and hit detection functionality.
|
||||
|
||||
@ -73,8 +83,8 @@ class ElementSelectionMixin:
|
||||
# Simple bounds check (no rotation transformation needed - images are already rotated)
|
||||
if ex <= page_x <= ex + ew and ey <= page_y <= ey + eh:
|
||||
# Store the renderer with the element for later use
|
||||
element._page_renderer = renderer
|
||||
element._parent_page = page
|
||||
element._page_renderer = renderer # type: ignore[attr-defined]
|
||||
element._parent_page = page # type: ignore[attr-defined]
|
||||
return element
|
||||
|
||||
return None
|
||||
@ -103,7 +113,7 @@ class ElementSelectionMixin:
|
||||
if not hasattr(self.selected_element, "_page_renderer"):
|
||||
return None
|
||||
|
||||
renderer = self.selected_element._page_renderer
|
||||
renderer: Any = self.selected_element._page_renderer # type: ignore[attr-defined]
|
||||
|
||||
# Get element position and size in page-local coordinates
|
||||
elem_x, elem_y = self.selected_element.position
|
||||
|
||||
@ -2,11 +2,16 @@
|
||||
Image pan mixin for GLWidget - handles panning images within frames
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
|
||||
from pyPhotoAlbum.models import ImageData
|
||||
|
||||
|
||||
class ImagePanMixin:
|
||||
# Type hints for expected attributes from mixing class
|
||||
drag_start_pos: Optional[Tuple[float, float]]
|
||||
zoom_level: float
|
||||
"""
|
||||
Mixin providing image panning functionality.
|
||||
|
||||
@ -21,7 +26,7 @@ class ImagePanMixin:
|
||||
self.image_pan_mode: bool = False # True when Control+dragging an ImageData element
|
||||
self.image_pan_start_crop: Optional[Tuple[float, float, float, float]] = None # Starting crop_info
|
||||
|
||||
def _handle_image_pan_move(self, x: float, y: float, element: ImageData):
|
||||
def _handle_image_pan_move(self, x: float, y: float, element: "ImageData"):
|
||||
"""
|
||||
Handle image panning within a frame during mouse move.
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ File operations mixin for pyPhotoAlbum
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional, cast
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QFileDialog,
|
||||
@ -18,6 +19,8 @@ from PyQt6.QtWidgets import (
|
||||
QButtonGroup,
|
||||
QLineEdit,
|
||||
QTextEdit,
|
||||
QWidget,
|
||||
QMessageBox,
|
||||
)
|
||||
from pyPhotoAlbum.decorators import ribbon_action, numerical_input
|
||||
from pyPhotoAlbum.project import Project, Page
|
||||
@ -32,6 +35,19 @@ from pyPhotoAlbum.asset_heal_dialog import AssetHealDialog
|
||||
class FileOperationsMixin:
|
||||
"""Mixin providing file-related operations"""
|
||||
|
||||
# Type hints for expected attributes from mixing class
|
||||
def show_status(self, message: str, timeout: int = 0) -> None:
|
||||
"""Expected from ApplicationStateMixin"""
|
||||
...
|
||||
|
||||
def show_error(self, title: str, message: str) -> None:
|
||||
"""Expected from ApplicationStateMixin"""
|
||||
...
|
||||
|
||||
def resolve_asset_path(self, path: str) -> Optional[str]:
|
||||
"""Expected from asset path mixin"""
|
||||
...
|
||||
|
||||
@ribbon_action(label="New", tooltip="Create a new project", tab="Home", group="File", shortcut="Ctrl+N")
|
||||
def new_project(self):
|
||||
"""Create a new project with initial setup dialog"""
|
||||
@ -324,7 +340,7 @@ class FileOperationsMixin:
|
||||
asset_list = "\n".join(f" • {path}" for path in missing_assets[:5])
|
||||
asset_list += f"\n ... and {len(missing_assets) - 5} more"
|
||||
|
||||
msg = QMessageBox(self)
|
||||
msg = QMessageBox(cast(QWidget, self))
|
||||
msg.setIcon(QMessageBox.Icon.Warning)
|
||||
msg.setWindowTitle("Missing Assets")
|
||||
msg.setText(f"{len(missing_assets)} image(s) could not be found in the assets folder:")
|
||||
|
||||
@ -2,10 +2,24 @@
|
||||
Page navigation mixin for GLWidget - handles page detection and ghost pages
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple, List
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt6.QtWidgets import QMainWindow
|
||||
|
||||
|
||||
class PageNavigationMixin:
|
||||
# Type hints for expected attributes from mixing class
|
||||
pan_offset: Tuple[float, float]
|
||||
zoom_level: float
|
||||
|
||||
def update(self) -> None:
|
||||
"""Expected from QWidget"""
|
||||
...
|
||||
|
||||
def window(self) -> "QMainWindow":
|
||||
"""Expected from QWidget"""
|
||||
...
|
||||
"""
|
||||
Mixin providing page navigation and ghost page functionality.
|
||||
|
||||
|
||||
@ -2,7 +2,11 @@
|
||||
Page layout and template system for pyPhotoAlbum
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from typing import List, Dict, Any, Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt6.QtWidgets import QWidget
|
||||
|
||||
from pyPhotoAlbum.models import BaseLayoutElement, ImageData, PlaceholderData, TextBoxData
|
||||
from pyPhotoAlbum.snapping import SnappingSystem
|
||||
from pyPhotoAlbum.gl_imports import (
|
||||
@ -45,6 +49,7 @@ class PageLayout:
|
||||
self.background_color = (1.0, 1.0, 1.0) # White background
|
||||
self.snapping_system = SnappingSystem()
|
||||
self.show_snap_lines = True # Show snap lines while dragging
|
||||
self._parent_widget: Optional["QWidget"] = None # Set by renderer
|
||||
|
||||
def add_element(self, element: BaseLayoutElement):
|
||||
"""Add a layout element to the page"""
|
||||
@ -251,9 +256,10 @@ class PageLayout:
|
||||
|
||||
# Deserialize elements and sort by z_index to establish list order
|
||||
# This ensures backward compatibility with projects that used z_index
|
||||
elem_list = []
|
||||
elem_list: List[BaseLayoutElement] = []
|
||||
for elem_data in data.get("elements", []):
|
||||
elem_type = elem_data.get("type")
|
||||
elem: BaseLayoutElement
|
||||
if elem_type == "image":
|
||||
elem = ImageData()
|
||||
elif elem_type == "placeholder":
|
||||
|
||||
@ -3,7 +3,7 @@ PDF export functionality for pyPhotoAlbum
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List, Tuple, Optional
|
||||
from typing import Any, List, Tuple, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.pdfgen import canvas
|
||||
@ -44,7 +44,7 @@ class SplitRenderParams:
|
||||
"""Parameters for rendering a split element"""
|
||||
|
||||
canvas: canvas.Canvas
|
||||
element: any
|
||||
element: Any
|
||||
x_offset_mm: float
|
||||
split_line_mm: float
|
||||
page_width_pt: float
|
||||
@ -71,7 +71,7 @@ class PDFExporter:
|
||||
"""
|
||||
self.project = project
|
||||
self.export_dpi = export_dpi
|
||||
self.warnings = []
|
||||
self.warnings: List[str] = []
|
||||
self.current_pdf_page = 1
|
||||
|
||||
def export(self, output_path: str, progress_callback=None) -> Tuple[bool, List[str]]:
|
||||
@ -293,7 +293,7 @@ class PDFExporter:
|
||||
x_offset_mm: float,
|
||||
page_width_pt: float,
|
||||
page_height_pt: float,
|
||||
page_number: int,
|
||||
page_number: Union[int, str],
|
||||
):
|
||||
"""
|
||||
Render a single element on the PDF canvas.
|
||||
|
||||
@ -6,7 +6,8 @@ import os
|
||||
import math
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import List, Dict, Any, Optional, Tuple, Union
|
||||
from pyPhotoAlbum.page_layout import PageLayout
|
||||
from pyPhotoAlbum.commands import CommandHistory
|
||||
from pyPhotoAlbum.asset_manager import AssetManager
|
||||
@ -170,7 +171,7 @@ class Project:
|
||||
|
||||
# Temporary directory management (if loaded from .ppz)
|
||||
# Using TemporaryDirectory instance that auto-cleans on deletion
|
||||
self._temp_dir = None
|
||||
self._temp_dir: Optional[TemporaryDirectory[str]] = None
|
||||
|
||||
# Global snapping settings (apply to all pages)
|
||||
self.snap_to_grid = False
|
||||
@ -323,7 +324,7 @@ class Project:
|
||||
else:
|
||||
return f"Page {adjusted_num}"
|
||||
|
||||
def calculate_page_layout_with_ghosts(self) -> List[Tuple[str, Any, int]]:
|
||||
def calculate_page_layout_with_ghosts(self) -> List[Tuple[str, Optional["Page"], int]]:
|
||||
"""
|
||||
Calculate page layout including ghost pages for alignment.
|
||||
Excludes cover from spread calculations.
|
||||
|
||||
@ -41,7 +41,7 @@ def build_ribbon_config(window_class: Type) -> Dict[str, Any]:
|
||||
}
|
||||
"""
|
||||
# Structure to collect actions by tab and group
|
||||
tabs = defaultdict(lambda: defaultdict(list))
|
||||
tabs: Dict[str, Dict[str, List[Dict[str, Any]]]] = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
# Scan all methods in the class and its bases (mixins)
|
||||
for attr_name in dir(window_class):
|
||||
|
||||
@ -4,7 +4,7 @@ Provides grid snapping, edge snapping, and custom guide snapping
|
||||
"""
|
||||
|
||||
import math
|
||||
from typing import List, Tuple, Optional
|
||||
from typing import Any, Dict, List, Tuple, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ class SnapResizeParams:
|
||||
resize_handle: str
|
||||
page_size: Tuple[float, float]
|
||||
dpi: int = 300
|
||||
project: Optional[any] = None
|
||||
project: Optional[Any] = None
|
||||
|
||||
|
||||
class SnappingSystem:
|
||||
@ -333,12 +333,12 @@ class SnappingSystem:
|
||||
snap_to_guides = self.snap_to_guides
|
||||
grid_size_mm = self.grid_size_mm
|
||||
|
||||
snap_candidates = []
|
||||
snap_candidates: List[Tuple[float, float]] = []
|
||||
|
||||
# 1. Page edge snapping
|
||||
if snap_to_edges:
|
||||
# Snap to start edge (0)
|
||||
snap_candidates.append((0, abs(edge_position - 0)))
|
||||
snap_candidates.append((0.0, abs(edge_position - 0)))
|
||||
|
||||
# Snap to end edge
|
||||
page_size_px = page_size_mm * dpi / 25.4
|
||||
@ -387,12 +387,12 @@ class SnappingSystem:
|
||||
Returns:
|
||||
Snapped position in pixels
|
||||
"""
|
||||
snap_candidates = []
|
||||
snap_candidates: List[Tuple[float, float]] = []
|
||||
|
||||
# 1. Page edge snapping
|
||||
if self.snap_to_edges:
|
||||
# Snap to start edge (0)
|
||||
snap_candidates.append((0, abs(position - 0)))
|
||||
snap_candidates.append((0.0, abs(position - 0)))
|
||||
|
||||
# Snap to end edge
|
||||
page_size_px = page_size_mm * dpi / 25.4
|
||||
@ -453,20 +453,20 @@ class SnappingSystem:
|
||||
page_width_px = page_width_mm * dpi / 25.4
|
||||
page_height_px = page_height_mm * dpi / 25.4
|
||||
|
||||
result = {"grid": [], "edges": [], "guides": []}
|
||||
result: Dict[str, List[Tuple[str, float]]] = {"grid": [], "edges": [], "guides": []}
|
||||
|
||||
# Grid lines
|
||||
if self.snap_to_grid:
|
||||
grid_size_px = self.grid_size_mm * dpi / 25.4
|
||||
|
||||
# Vertical grid lines
|
||||
x = 0
|
||||
x: float = 0
|
||||
while x <= page_width_px:
|
||||
result["grid"].append(("vertical", x))
|
||||
x += grid_size_px
|
||||
|
||||
# Horizontal grid lines
|
||||
y = 0
|
||||
y: float = 0
|
||||
while y <= page_height_px:
|
||||
result["grid"].append(("horizontal", y))
|
||||
y += grid_size_px
|
||||
|
||||
@ -47,6 +47,7 @@ class Template:
|
||||
# Deserialize elements
|
||||
for elem_data in data.get("elements", []):
|
||||
elem_type = elem_data.get("type")
|
||||
elem: BaseLayoutElement
|
||||
if elem_type == "placeholder":
|
||||
elem = PlaceholderData()
|
||||
elif elem_type == "textbox":
|
||||
@ -316,9 +317,10 @@ class TemplateManager:
|
||||
offset_x = (to_width - from_width) / 2
|
||||
offset_y = (to_height - from_height) / 2
|
||||
|
||||
scaled_elements = []
|
||||
scaled_elements: List[BaseLayoutElement] = []
|
||||
for element in elements:
|
||||
# Create a new element of the same type
|
||||
new_elem: BaseLayoutElement
|
||||
if isinstance(element, PlaceholderData):
|
||||
new_elem = PlaceholderData(
|
||||
placeholder_type=element.placeholder_type, default_content=element.default_content
|
||||
|
||||
414
tests/test_text_edit_dialog.py
Normal file
414
tests/test_text_edit_dialog.py
Normal file
@ -0,0 +1,414 @@
|
||||
"""
|
||||
Tests for text_edit_dialog module
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
from PyQt6.QtGui import QColor
|
||||
|
||||
|
||||
class TestTextEditDialogInit:
|
||||
"""Tests for TextEditDialog initialization"""
|
||||
|
||||
def test_init_basic(self, qtbot):
|
||||
"""Test basic dialog initialization"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = "Hello World"
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.text_element == mock_text_element
|
||||
assert dialog.windowTitle() == "Edit Text"
|
||||
|
||||
def test_init_loads_text_content(self, qtbot):
|
||||
"""Test that init loads text content from element"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = "Test content here"
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 14, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "center"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.text_edit.toPlainText() == "Test content here"
|
||||
|
||||
def test_init_loads_font_family(self, qtbot):
|
||||
"""Test that init loads font family from element"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Georgia", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.font_combo.currentText() == "Georgia"
|
||||
|
||||
def test_init_loads_font_size(self, qtbot):
|
||||
"""Test that init loads font size from element"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 24, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.font_size_spin.value() == 24
|
||||
|
||||
def test_init_loads_color_255_range(self, qtbot):
|
||||
"""Test that init loads color in 0-255 range"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (255, 128, 64)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.current_color.red() == 255
|
||||
assert dialog.current_color.green() == 128
|
||||
assert dialog.current_color.blue() == 64
|
||||
|
||||
def test_init_loads_color_01_range(self, qtbot):
|
||||
"""Test that init loads color in 0-1 range"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (1.0, 0.5, 0.0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.current_color.red() == 255
|
||||
assert dialog.current_color.green() == 127
|
||||
assert dialog.current_color.blue() == 0
|
||||
|
||||
def test_init_loads_alignment(self, qtbot):
|
||||
"""Test that init loads alignment from element"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "right"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.alignment_combo.currentText() == "right"
|
||||
|
||||
def test_init_handles_unknown_font(self, qtbot):
|
||||
"""Test that init handles unknown font family gracefully"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "UnknownFont", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should not crash, will keep default selection
|
||||
assert dialog.font_combo.currentIndex() >= 0
|
||||
|
||||
def test_init_handles_default_values(self, qtbot):
|
||||
"""Test that init handles missing font settings"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {} # Empty settings
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Should use defaults
|
||||
assert dialog.font_size_spin.value() == 12
|
||||
|
||||
|
||||
class TestTextEditDialogUI:
|
||||
"""Tests for TextEditDialog UI elements"""
|
||||
|
||||
def test_font_size_range(self, qtbot):
|
||||
"""Test that font size spinner has correct range"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
assert dialog.font_size_spin.minimum() == 6
|
||||
assert dialog.font_size_spin.maximum() == 72
|
||||
|
||||
def test_alignment_options(self, qtbot):
|
||||
"""Test that alignment combo has correct options"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
options = [dialog.alignment_combo.itemText(i) for i in range(dialog.alignment_combo.count())]
|
||||
assert "left" in options
|
||||
assert "center" in options
|
||||
assert "right" in options
|
||||
|
||||
def test_font_options(self, qtbot):
|
||||
"""Test that font combo has expected fonts"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
fonts = [dialog.font_combo.itemText(i) for i in range(dialog.font_combo.count())]
|
||||
assert "Arial" in fonts
|
||||
assert "Times New Roman" in fonts
|
||||
assert "Courier New" in fonts
|
||||
|
||||
|
||||
class TestChooseColor:
|
||||
"""Tests for _choose_color method"""
|
||||
|
||||
def test_choose_color_updates_current_color(self, qtbot):
|
||||
"""Test that choosing color updates current_color"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Mock QColorDialog to return a specific color
|
||||
with patch("pyPhotoAlbum.text_edit_dialog.QColorDialog.getColor") as mock_get_color:
|
||||
mock_color = QColor(255, 0, 0)
|
||||
mock_get_color.return_value = mock_color
|
||||
|
||||
dialog._choose_color()
|
||||
|
||||
assert dialog.current_color.red() == 255
|
||||
assert dialog.current_color.green() == 0
|
||||
assert dialog.current_color.blue() == 0
|
||||
|
||||
def test_choose_color_invalid_does_not_update(self, qtbot):
|
||||
"""Test that invalid color choice does not update current_color"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
original_color = QColor(dialog.current_color)
|
||||
|
||||
# Mock QColorDialog to return invalid color (user cancelled)
|
||||
with patch("pyPhotoAlbum.text_edit_dialog.QColorDialog.getColor") as mock_get_color:
|
||||
mock_get_color.return_value = QColor() # Invalid color
|
||||
|
||||
dialog._choose_color()
|
||||
|
||||
# Color should remain unchanged
|
||||
assert dialog.current_color == original_color
|
||||
|
||||
|
||||
class TestUpdateColorButton:
|
||||
"""Tests for _update_color_button method"""
|
||||
|
||||
def test_update_color_button_dark_color(self, qtbot):
|
||||
"""Test color button with dark color uses white text"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
dialog.current_color = QColor(0, 0, 0) # Black
|
||||
dialog._update_color_button()
|
||||
|
||||
stylesheet = dialog.color_button.styleSheet()
|
||||
assert "color: white" in stylesheet
|
||||
|
||||
def test_update_color_button_light_color(self, qtbot):
|
||||
"""Test color button with light color uses black text"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (255, 255, 255)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
dialog.current_color = QColor(255, 255, 255) # White
|
||||
dialog._update_color_button()
|
||||
|
||||
stylesheet = dialog.color_button.styleSheet()
|
||||
assert "color: black" in stylesheet
|
||||
|
||||
|
||||
class TestGetValues:
|
||||
"""Tests for get_values method"""
|
||||
|
||||
def test_get_values_returns_text(self, qtbot):
|
||||
"""Test get_values returns text content"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = "Original"
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
dialog.text_edit.setPlainText("New text content")
|
||||
|
||||
values = dialog.get_values()
|
||||
assert values["text_content"] == "New text content"
|
||||
|
||||
def test_get_values_returns_font_settings(self, qtbot):
|
||||
"""Test get_values returns font settings"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
dialog.font_combo.setCurrentText("Georgia")
|
||||
dialog.font_size_spin.setValue(18)
|
||||
dialog.current_color = QColor(100, 150, 200)
|
||||
|
||||
values = dialog.get_values()
|
||||
assert values["font_settings"]["family"] == "Georgia"
|
||||
assert values["font_settings"]["size"] == 18
|
||||
assert values["font_settings"]["color"] == (100, 150, 200)
|
||||
|
||||
def test_get_values_returns_alignment(self, qtbot):
|
||||
"""Test get_values returns alignment"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
dialog.alignment_combo.setCurrentText("center")
|
||||
|
||||
values = dialog.get_values()
|
||||
assert values["alignment"] == "center"
|
||||
|
||||
def test_get_values_complete_structure(self, qtbot):
|
||||
"""Test get_values returns complete structure"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = "Test"
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
values = dialog.get_values()
|
||||
|
||||
# Check structure
|
||||
assert "text_content" in values
|
||||
assert "font_settings" in values
|
||||
assert "alignment" in values
|
||||
assert "family" in values["font_settings"]
|
||||
assert "size" in values["font_settings"]
|
||||
assert "color" in values["font_settings"]
|
||||
|
||||
|
||||
class TestDialogButtons:
|
||||
"""Tests for dialog buttons"""
|
||||
|
||||
def test_cancel_button_rejects(self, qtbot):
|
||||
"""Test that cancel button rejects the dialog"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
from PyQt6.QtWidgets import QPushButton
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Find cancel button
|
||||
buttons = dialog.findChildren(QPushButton)
|
||||
cancel_button = next(b for b in buttons if b.text() == "Cancel")
|
||||
|
||||
# Click should reject
|
||||
with qtbot.waitSignal(dialog.rejected, timeout=1000):
|
||||
cancel_button.click()
|
||||
|
||||
def test_ok_button_accepts(self, qtbot):
|
||||
"""Test that OK button accepts the dialog"""
|
||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||
from PyQt6.QtWidgets import QPushButton
|
||||
|
||||
mock_text_element = Mock()
|
||||
mock_text_element.text_content = ""
|
||||
mock_text_element.font_settings = {"family": "Arial", "size": 12, "color": (0, 0, 0)}
|
||||
mock_text_element.alignment = "left"
|
||||
|
||||
dialog = TextEditDialog(mock_text_element)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
# Find OK button
|
||||
buttons = dialog.findChildren(QPushButton)
|
||||
ok_button = next(b for b in buttons if b.text() == "OK")
|
||||
|
||||
# Click should accept
|
||||
with qtbot.waitSignal(dialog.accepted, timeout=1000):
|
||||
ok_button.click()
|
||||
Loading…
x
Reference in New Issue
Block a user