diff --git a/tests/test_file_ops_mixin.py b/tests/test_file_ops_mixin.py index d190bb5..f7f447a 100644 --- a/tests/test_file_ops_mixin.py +++ b/tests/test_file_ops_mixin.py @@ -257,25 +257,50 @@ class TestLoadCallbacks: class TestSaveProject: """Test save_project method""" - @patch("pyPhotoAlbum.mixins.operations.file_ops.save_to_zip") - def test_save_project_with_existing_path(self, mock_save, qtbot): + @patch("pyPhotoAlbum.mixins.operations.file_ops.save_to_zip_async") + @patch("pyPhotoAlbum.mixins.operations.file_ops.LoadingWidget") + def test_save_project_with_existing_path(self, mock_loading_widget, mock_save_async, qtbot): """Test saves to existing file path""" window = TestFileOpsWindow() qtbot.addWidget(window) window.project.file_path = "/path/to/existing.ppz" - mock_save.return_value = (True, None) + + # Mock loading widget + mock_loading_instance = Mock() + mock_loading_widget.return_value = mock_loading_instance + + # Capture status messages + status_messages = [] + original_show_status = window.show_status + def capture_status(msg, timeout=0): + status_messages.append(msg) + original_show_status(msg, timeout) + window.show_status = capture_status + + # Mock save_to_zip_async to call on_complete immediately with success + def mock_save_call(project, path, on_complete=None, on_progress=None): + if on_complete: + on_complete(True, None) + return Mock() # Return mock thread + + mock_save_async.side_effect = mock_save_call window.save_project() # Verify save was called with existing path - mock_save.assert_called_once_with(window.project, "/path/to/existing.ppz") - assert "Project saved" in window._status_message + assert mock_save_async.call_count == 1 + call_args = mock_save_async.call_args + assert call_args[0][0] == window.project + assert call_args[0][1] == "/path/to/existing.ppz" + # Check that "Project saved" was shown at some point + assert any("Project saved" in msg for msg in status_messages) assert not window.project.is_dirty() # is_dirty() is a method - @patch("pyPhotoAlbum.mixins.operations.file_ops.save_to_zip") + @patch("pyPhotoAlbum.mixins.operations.file_ops.save_to_zip_async") + @patch("pyPhotoAlbum.mixins.operations.file_ops.LoadingWidget") @patch("pyPhotoAlbum.mixins.operations.file_ops.QFileDialog.getSaveFileName") - def test_save_project_prompts_for_path(self, mock_file_dialog, mock_save, qtbot): + def test_save_project_prompts_for_path(self, mock_file_dialog, mock_loading_widget, mock_save_async, qtbot): """Test prompts for path when none exists""" window = TestFileOpsWindow() qtbot.addWidget(window) @@ -285,18 +310,33 @@ class TestSaveProject: # Mock dialog to return path mock_file_dialog.return_value = ("/path/to/new.ppz", "") - mock_save.return_value = (True, None) + + # Mock loading widget + mock_loading_instance = Mock() + mock_loading_widget.return_value = mock_loading_instance + + # Mock save_to_zip_async to call on_complete immediately with success + def mock_save_call(project, path, on_complete=None, on_progress=None): + if on_complete: + on_complete(True, None) + return Mock() # Return mock thread + + mock_save_async.side_effect = mock_save_call window.save_project() # Verify dialog was shown and save was called mock_file_dialog.assert_called_once() - mock_save.assert_called_once_with(window.project, "/path/to/new.ppz") + assert mock_save_async.call_count == 1 + call_args = mock_save_async.call_args + assert call_args[0][0] == window.project + assert call_args[0][1] == "/path/to/new.ppz" assert window.project.file_path == "/path/to/new.ppz" + assert not window.project.is_dirty() - @patch("pyPhotoAlbum.mixins.operations.file_ops.save_to_zip") + @patch("pyPhotoAlbum.mixins.operations.file_ops.save_to_zip_async") @patch("pyPhotoAlbum.mixins.operations.file_ops.QFileDialog.getSaveFileName") - def test_save_project_user_cancels(self, mock_file_dialog, mock_save, qtbot): + def test_save_project_user_cancels(self, mock_file_dialog, mock_save_async, qtbot): """Test returns when user cancels file dialog""" window = TestFileOpsWindow() qtbot.addWidget(window) @@ -307,22 +347,34 @@ class TestSaveProject: window.save_project() # Should not call save - mock_save.assert_not_called() + mock_save_async.assert_not_called() - @patch("pyPhotoAlbum.mixins.operations.file_ops.save_to_zip") - def test_save_project_handles_error(self, mock_save, qtbot): + @patch("pyPhotoAlbum.mixins.operations.file_ops.save_to_zip_async") + @patch("pyPhotoAlbum.mixins.operations.file_ops.LoadingWidget") + def test_save_project_handles_error(self, mock_loading_widget, mock_save_async, qtbot): """Test handles save errors""" window = TestFileOpsWindow() qtbot.addWidget(window) window.project.file_path = "/path/to/project.ppz" - mock_save.return_value = (False, "Disk full") + + # Mock loading widget + mock_loading_instance = Mock() + mock_loading_widget.return_value = mock_loading_instance + + # Mock save_to_zip_async to call on_complete with error + def mock_save_call(project, path, on_complete=None, on_progress=None): + if on_complete: + on_complete(False, "Disk full") + return Mock() # Return mock thread + + mock_save_async.side_effect = mock_save_call window.save_project() # Verify error was shown - assert "Failed to save project" in window._status_message - assert "Disk full" in window._status_message + assert "Failed to save project" in window._error_message + assert "Disk full" in window._error_message class TestHealAssets: diff --git a/tests/test_models.py b/tests/test_models.py index 51212c6..f693a24 100755 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1099,8 +1099,7 @@ class TestTextBoxData: @patch("pyPhotoAlbum.models.glTranslatef") @patch("pyPhotoAlbum.models.glDisable") @patch("pyPhotoAlbum.models.glEnable") - @patch("pyPhotoAlbum.models.glBlendFunc") - @patch("pyPhotoAlbum.models.glColor4f") + @patch("pyPhotoAlbum.models.glLineStipple") @patch("pyPhotoAlbum.models.glColor3f") @patch("pyPhotoAlbum.models.glVertex2f") @patch("pyPhotoAlbum.models.glBegin") @@ -1111,8 +1110,7 @@ class TestTextBoxData: mock_glBegin, mock_glVertex2f, mock_glColor3f, - mock_glColor4f, - mock_glBlendFunc, + mock_glLineStipple, mock_glEnable, mock_glDisable, mock_glTranslatef, @@ -1125,18 +1123,15 @@ class TestTextBoxData: textbox.render() - # Should enable and disable blending - from pyPhotoAlbum.models import GL_BLEND, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA + # Should enable and disable line stipple for dashed border + from pyPhotoAlbum.models import GL_LINE_STIPPLE - mock_glEnable.assert_called_with(GL_BLEND) - mock_glBlendFunc.assert_called_once_with(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - mock_glDisable.assert_called_with(GL_BLEND) + mock_glEnable.assert_called_with(GL_LINE_STIPPLE) + mock_glLineStipple.assert_called_once_with(2, 0xAAAA) + mock_glDisable.assert_called_with(GL_LINE_STIPPLE) - # Should draw semi-transparent yellow background - mock_glColor4f.assert_called_with(1.0, 1.0, 0.7, 0.3) - - # Should draw black border - mock_glColor3f.assert_called_with(0.0, 0.0, 0.0) + # Should draw light gray dashed border + mock_glColor3f.assert_called_with(0.7, 0.7, 0.7) # Should NOT push/pop matrix when rotation is 0 mock_glPushMatrix.assert_not_called() diff --git a/tests/test_page_layout_extended.py b/tests/test_page_layout_extended.py index c40171d..46a0788 100644 --- a/tests/test_page_layout_extended.py +++ b/tests/test_page_layout_extended.py @@ -313,9 +313,10 @@ class TestPageLayoutSnapLines: layout.render(dpi=300, project=mock_project) - # Cyan color should be used for guides (0.0, 0.7, 0.9, 0.8) + # Cyan color should be used for guides (0.0, 0.7, 0.9) # Check if cyan color was set (at least once) - assert mock_color4f.call_count > 0 + cyan_calls = [call for call in mock_color3f.call_args_list if call[0] == (0.0, 0.7, 0.9)] + assert len(cyan_calls) > 0, "Cyan color for guides should be set" class TestPageLayoutEdgeCases: