pyPhotoAlbum/tests/test_merge_dialog.py
Duncan Tourolle b18a780a33
All checks were successful
Python CI / test (push) Successful in 1m28s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m41s
Tests / test (3.12) (push) Successful in 1m42s
Tests / test (3.13) (push) Successful in 1m35s
Tests / test (3.14) (push) Successful in 1m15s
increase test coverage
2025-11-28 19:54:41 +01:00

606 lines
22 KiB
Python

"""
Tests for merge_dialog module
"""
import pytest
from unittest.mock import Mock, MagicMock, patch
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QRadioButton
class TestPagePreviewWidget:
"""Tests for PagePreviewWidget class"""
def test_init(self, qtbot):
"""Test PagePreviewWidget initialization"""
from pyPhotoAlbum.merge_dialog import PagePreviewWidget
page_data = {
"page_number": 1,
"layout": {"elements": []},
"last_modified": "2024-01-01 12:00:00",
}
widget = PagePreviewWidget(page_data)
qtbot.addWidget(widget)
assert widget.page_data == page_data
assert widget.minimumSize().width() == 200
assert widget.minimumSize().height() == 280
def test_paint_event_basic(self, qtbot):
"""Test basic paint event rendering"""
from pyPhotoAlbum.merge_dialog import PagePreviewWidget
page_data = {
"page_number": 2,
"layout": {"elements": []},
"last_modified": "2024-01-01",
}
widget = PagePreviewWidget(page_data)
qtbot.addWidget(widget)
widget.show()
# Trigger paint event
widget.repaint()
def test_paint_event_with_elements(self, qtbot):
"""Test paint event with elements"""
from pyPhotoAlbum.merge_dialog import PagePreviewWidget
page_data = {
"page_number": 3,
"layout": {
"elements": [
{"type": "image", "deleted": False},
{"type": "textbox", "deleted": False},
{"type": "shape", "deleted": True},
]
},
"last_modified": "2024-01-01 10:00:00",
}
widget = PagePreviewWidget(page_data)
qtbot.addWidget(widget)
widget.show()
widget.repaint()
def test_paint_event_many_elements(self, qtbot):
"""Test paint event with more than 5 elements (tests truncation)"""
from pyPhotoAlbum.merge_dialog import PagePreviewWidget
elements = [{"type": f"elem{i}", "deleted": False} for i in range(10)]
page_data = {
"page_number": 4,
"layout": {"elements": elements},
"last_modified": "2024-01-01",
}
widget = PagePreviewWidget(page_data)
qtbot.addWidget(widget)
widget.show()
widget.repaint()
class TestConflictItemWidget:
"""Tests for ConflictItemWidget class"""
@pytest.fixture
def mock_conflict_page(self):
"""Create a mock page conflict"""
from pyPhotoAlbum.merge_manager import ConflictInfo, ConflictType
conflict = ConflictInfo(
conflict_type=ConflictType.PAGE_MODIFIED_BOTH,
page_uuid="page-uuid-1",
element_uuid=None,
description="Page 1 modified in both versions",
our_version={
"page_number": 1,
"layout": {"elements": [{"type": "image"}]},
"last_modified": "2024-01-01 10:00:00",
},
their_version={
"page_number": 1,
"layout": {"elements": [{"type": "image"}, {"type": "textbox"}]},
"last_modified": "2024-01-01 11:00:00",
},
)
return conflict
@pytest.fixture
def mock_conflict_element(self):
"""Create a mock element conflict"""
from pyPhotoAlbum.merge_manager import ConflictInfo, ConflictType
conflict = ConflictInfo(
conflict_type=ConflictType.ELEMENT_MODIFIED_BOTH,
page_uuid="page-uuid-1",
element_uuid="element-uuid-1",
description="Element modified in both versions",
our_version={
"type": "image",
"position": (10, 20),
"size": (100, 100),
"deleted": False,
"last_modified": "2024-01-01",
},
their_version={
"type": "image",
"position": (15, 25),
"size": (120, 120),
"deleted": False,
"last_modified": "2024-01-02",
},
)
return conflict
@pytest.fixture
def mock_conflict_settings(self):
"""Create a mock settings conflict"""
from pyPhotoAlbum.merge_manager import ConflictInfo, ConflictType
conflict = ConflictInfo(
conflict_type=ConflictType.SETTINGS_MODIFIED_BOTH,
page_uuid=None,
element_uuid=None,
description="Settings modified",
our_version={"theme": "light", "font_size": 12, "last_modified": "2024-01-01"},
their_version={"theme": "dark", "font_size": 14, "last_modified": "2024-01-02"},
)
return conflict
def test_init_page_conflict(self, qtbot, mock_conflict_page):
"""Test initialization with page conflict"""
from pyPhotoAlbum.merge_dialog import ConflictItemWidget
widget = ConflictItemWidget(0, mock_conflict_page)
qtbot.addWidget(widget)
assert widget.conflict_index == 0
assert widget.conflict == mock_conflict_page
def test_init_element_conflict(self, qtbot, mock_conflict_element):
"""Test initialization with element conflict"""
from pyPhotoAlbum.merge_dialog import ConflictItemWidget
widget = ConflictItemWidget(1, mock_conflict_element)
qtbot.addWidget(widget)
assert widget.conflict_index == 1
assert widget.conflict == mock_conflict_element
def test_init_settings_conflict(self, qtbot, mock_conflict_settings):
"""Test initialization with settings conflict"""
from pyPhotoAlbum.merge_dialog import ConflictItemWidget
widget = ConflictItemWidget(2, mock_conflict_settings)
qtbot.addWidget(widget)
assert widget.conflict_index == 2
def test_resolution_changed_signal_ours(self, qtbot, mock_conflict_page):
"""Test resolution_changed signal for 'ours'"""
from pyPhotoAlbum.merge_dialog import ConflictItemWidget
widget = ConflictItemWidget(0, mock_conflict_page)
qtbot.addWidget(widget)
signal_received = []
def on_resolution_changed(index, choice):
signal_received.append((index, choice))
widget.resolution_changed.connect(on_resolution_changed)
# First select "theirs", then select "ours" to trigger the signal
for button in widget.button_group.buttons():
if "Other" in button.text():
button.setChecked(True)
break
# Clear the signal list
signal_received.clear()
# Now select "ours" to trigger the signal
for button in widget.button_group.buttons():
if "Your" in button.text():
button.setChecked(True)
break
assert len(signal_received) == 1
assert signal_received[0] == (0, "ours")
def test_resolution_changed_signal_theirs(self, qtbot, mock_conflict_page):
"""Test resolution_changed signal for 'theirs'"""
from pyPhotoAlbum.merge_dialog import ConflictItemWidget
widget = ConflictItemWidget(0, mock_conflict_page)
qtbot.addWidget(widget)
signal_received = []
def on_resolution_changed(index, choice):
signal_received.append((index, choice))
widget.resolution_changed.connect(on_resolution_changed)
# Find "Use Other Version" button and click it
for button in widget.button_group.buttons():
if "Other" in button.text():
button.setChecked(True)
break
assert len(signal_received) == 1
assert signal_received[0] == (0, "theirs")
def test_get_resolution_ours(self, qtbot, mock_conflict_page):
"""Test get_resolution returns 'ours' when selected"""
from pyPhotoAlbum.merge_dialog import ConflictItemWidget
widget = ConflictItemWidget(0, mock_conflict_page)
qtbot.addWidget(widget)
# Default is "ours"
assert widget.get_resolution() == "ours"
def test_get_resolution_theirs(self, qtbot, mock_conflict_page):
"""Test get_resolution returns 'theirs' when selected"""
from pyPhotoAlbum.merge_dialog import ConflictItemWidget
widget = ConflictItemWidget(0, mock_conflict_page)
qtbot.addWidget(widget)
# Select "theirs"
for button in widget.button_group.buttons():
if "Other" in button.text():
button.setChecked(True)
break
assert widget.get_resolution() == "theirs"
def test_create_element_details_image(self, qtbot, mock_conflict_element):
"""Test creating element details for image element"""
from pyPhotoAlbum.merge_dialog import ConflictItemWidget
widget = ConflictItemWidget(0, mock_conflict_element)
qtbot.addWidget(widget)
details = widget._create_element_details(mock_conflict_element.our_version)
assert details is not None
assert "Type: image" in details.toPlainText()
def test_create_element_details_textbox(self, qtbot):
"""Test creating element details for textbox element"""
from pyPhotoAlbum.merge_dialog import ConflictItemWidget
from pyPhotoAlbum.merge_manager import ConflictInfo, ConflictType
element_data = {
"type": "textbox",
"position": (0, 0),
"size": (100, 50),
"deleted": False,
"last_modified": "2024-01-01",
"text_content": "This is a long text that should be truncated after 50 characters for display",
}
conflict = ConflictInfo(
conflict_type=ConflictType.ELEMENT_MODIFIED_BOTH,
page_uuid="page-uuid-1",
element_uuid="element-uuid-2",
description="Text element",
our_version=element_data,
their_version=element_data,
)
widget = ConflictItemWidget(0, conflict)
qtbot.addWidget(widget)
details = widget._create_element_details(element_data)
text = details.toPlainText()
assert "Type: textbox" in text
assert "Text:" in text
def test_create_settings_details(self, qtbot, mock_conflict_settings):
"""Test creating settings details"""
from pyPhotoAlbum.merge_dialog import ConflictItemWidget
widget = ConflictItemWidget(0, mock_conflict_settings)
qtbot.addWidget(widget)
details = widget._create_settings_details(mock_conflict_settings.our_version)
assert details is not None
text = details.toPlainText()
assert "theme: light" in text
assert "font_size: 12" in text
assert "Modified:" in text
class TestMergeDialog:
"""Tests for MergeDialog class"""
@pytest.fixture
def our_project_data(self):
"""Create mock 'our' project data"""
return {
"name": "Our Project",
"last_modified": "2024-01-01 10:00:00",
"pages": [
{
"page_number": 1,
"layout": {"elements": [{"type": "image"}]},
"last_modified": "2024-01-01 10:00:00",
}
],
}
@pytest.fixture
def their_project_data(self):
"""Create mock 'their' project data"""
return {
"name": "Their Project",
"last_modified": "2024-01-01 11:00:00",
"pages": [
{
"page_number": 1,
"layout": {"elements": [{"type": "image"}, {"type": "textbox"}]},
"last_modified": "2024-01-01 11:00:00",
}
],
}
@pytest.fixture
def mock_conflicts(self):
"""Create mock conflicts"""
from pyPhotoAlbum.merge_manager import ConflictInfo, ConflictType
return [
ConflictInfo(
conflict_type=ConflictType.PAGE_MODIFIED_BOTH,
page_uuid="page-uuid-conflict",
element_uuid=None,
description="Page conflict",
our_version={"page_number": 1, "layout": {"elements": []}},
their_version={"page_number": 1, "layout": {"elements": [{"type": "image"}]}},
)
]
def test_init(self, qtbot, our_project_data, their_project_data, mock_conflicts):
"""Test MergeDialog initialization"""
from pyPhotoAlbum.merge_dialog import MergeDialog
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = mock_conflicts
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
assert dialog.our_project_data == our_project_data
assert dialog.their_project_data == their_project_data
assert len(dialog.conflicts) == 1
assert len(dialog.resolutions) == 1
assert dialog.resolutions[0] == "ours" # Default
def test_init_ui(self, qtbot, our_project_data, their_project_data, mock_conflicts):
"""Test UI initialization"""
from pyPhotoAlbum.merge_dialog import MergeDialog
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = mock_conflicts
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
assert dialog.windowTitle() == "Merge Projects"
assert dialog.strategy_combo is not None
assert len(dialog.conflict_widgets) == 1
def test_on_resolution_changed(self, qtbot, our_project_data, their_project_data, mock_conflicts):
"""Test handling resolution changes"""
from pyPhotoAlbum.merge_dialog import MergeDialog
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = mock_conflicts
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
dialog._on_resolution_changed(0, "theirs")
assert dialog.resolutions[0] == "theirs"
def test_auto_resolve_latest_wins(self, qtbot, our_project_data, their_project_data, mock_conflicts):
"""Test auto-resolve with LATEST_WINS strategy"""
from pyPhotoAlbum.merge_dialog import MergeDialog
from pyPhotoAlbum.merge_manager import MergeStrategy
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = mock_conflicts
mock_manager.auto_resolve_conflicts.return_value = {0: "theirs"}
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
# Set strategy to LATEST_WINS
dialog.strategy_combo.setCurrentIndex(0)
dialog._auto_resolve()
mock_manager.auto_resolve_conflicts.assert_called_once()
assert dialog.resolutions[0] == "theirs"
def test_auto_resolve_ours(self, qtbot, our_project_data, their_project_data, mock_conflicts):
"""Test auto-resolve with OURS strategy"""
from pyPhotoAlbum.merge_dialog import MergeDialog
from pyPhotoAlbum.merge_manager import MergeStrategy
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = mock_conflicts
mock_manager.auto_resolve_conflicts.return_value = {0: "ours"}
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
# Set strategy to OURS
dialog.strategy_combo.setCurrentIndex(1)
dialog._auto_resolve()
assert dialog.resolutions[0] == "ours"
def test_auto_resolve_theirs(self, qtbot, our_project_data, their_project_data, mock_conflicts):
"""Test auto-resolve with THEIRS strategy"""
from pyPhotoAlbum.merge_dialog import MergeDialog
from pyPhotoAlbum.merge_manager import MergeStrategy
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = mock_conflicts
mock_manager.auto_resolve_conflicts.return_value = {0: "theirs"}
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
# Set strategy to THEIRS
dialog.strategy_combo.setCurrentIndex(2)
dialog._auto_resolve()
assert dialog.resolutions[0] == "theirs"
def test_auto_resolve_updates_ui(self, qtbot, our_project_data, their_project_data, mock_conflicts):
"""Test that auto-resolve updates UI radio buttons"""
from pyPhotoAlbum.merge_dialog import MergeDialog
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = mock_conflicts
mock_manager.auto_resolve_conflicts.return_value = {0: "theirs"}
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
dialog._auto_resolve()
# Check that the "Other Version" button is now checked
conflict_widget = dialog.conflict_widgets[0]
resolution = conflict_widget.get_resolution()
assert resolution == "theirs"
def test_get_merged_project_data(self, qtbot, our_project_data, their_project_data, mock_conflicts):
"""Test getting merged project data"""
from pyPhotoAlbum.merge_dialog import MergeDialog
merged_data = {"name": "Merged", "pages": []}
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = mock_conflicts
mock_manager.apply_resolutions.return_value = merged_data
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
result = dialog.get_merged_project_data()
mock_manager.apply_resolutions.assert_called_once_with(
our_project_data, their_project_data, dialog.resolutions
)
assert result == merged_data
def test_accept_button(self, qtbot, our_project_data, their_project_data, mock_conflicts):
"""Test clicking Accept button"""
from pyPhotoAlbum.merge_dialog import MergeDialog
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = mock_conflicts
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
# This should trigger accept without errors
dialog.accept()
def test_reject_button(self, qtbot, our_project_data, their_project_data, mock_conflicts):
"""Test clicking Cancel button"""
from pyPhotoAlbum.merge_dialog import MergeDialog
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = mock_conflicts
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
# This should trigger reject without errors
dialog.reject()
def test_no_conflicts(self, qtbot, our_project_data, their_project_data):
"""Test dialog with no conflicts"""
from pyPhotoAlbum.merge_dialog import MergeDialog
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = []
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
assert len(dialog.conflicts) == 0
assert len(dialog.conflict_widgets) == 0
def test_multiple_conflicts(self, qtbot, our_project_data, their_project_data):
"""Test dialog with multiple conflicts"""
from pyPhotoAlbum.merge_dialog import MergeDialog
from pyPhotoAlbum.merge_manager import ConflictInfo, ConflictType
conflicts = [
ConflictInfo(
conflict_type=ConflictType.PAGE_MODIFIED_BOTH,
page_uuid=f"page-uuid-{i}",
element_uuid=None,
description=f"Conflict {i}",
our_version={"page_number": i},
their_version={"page_number": i},
)
for i in range(5)
]
with patch("pyPhotoAlbum.merge_dialog.MergeManager") as MockMergeManager:
mock_manager = Mock()
mock_manager.detect_conflicts.return_value = conflicts
MockMergeManager.return_value = mock_manager
dialog = MergeDialog(our_project_data, their_project_data)
qtbot.addWidget(dialog)
assert len(dialog.conflicts) == 5
assert len(dialog.conflict_widgets) == 5
assert len(dialog.resolutions) == 5
# All should default to "ours"
for i in range(5):
assert dialog.resolutions[i] == "ours"