""" 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"