Add save confirmation modal and integrate with entry editing
- Implemented SaveConfirmationModal to prompt users for saving changes when exiting edit mode. - Integrated modal into HostsManagerApp to handle unsaved changes. - Added methods to validate and save entry changes, restoring original values if discarded. - Created unit tests for SaveConfirmationModal and its integration with the main application. - Refactored entry editing logic to track changes and confirm before exiting edit mode.
This commit is contained in:
		
							parent
							
								
									5a117fb624
								
							
						
					
					
						commit
						f7671db43e
					
				
					 5 changed files with 761 additions and 221 deletions
				
			
		| 
						 | 
				
			
			@ -2,10 +2,19 @@
 | 
			
		|||
 | 
			
		||||
## Current Work Focus
 | 
			
		||||
 | 
			
		||||
**Phase 3 Complete - Edit Mode Foundation**: The hosts TUI application now has a complete edit mode foundation with permission management, entry manipulation, and safe file operations. All keyboard shortcuts are implemented and tested. The application is ready for Phase 4 advanced edit features.
 | 
			
		||||
**Phase 3 Complete with Save Confirmation Enhancement**: The hosts TUI application now has a complete edit mode foundation with permission management, entry manipulation, safe file operations, and professional save confirmation functionality. All keyboard shortcuts are implemented and tested. The application is ready for Phase 4 advanced edit features.
 | 
			
		||||
 | 
			
		||||
## Recent Changes
 | 
			
		||||
 | 
			
		||||
### Phase 3 Save Confirmation Enhancement ✅ COMPLETE
 | 
			
		||||
- ✅ **Save confirmation modal**: Professional modal dialog asking to save, discard, or cancel when exiting edit entry mode
 | 
			
		||||
- ✅ **Change detection system**: Intelligent tracking of original entry values vs. current form values 
 | 
			
		||||
- ✅ **No auto-save behavior**: Changes are only saved when explicitly confirmed by the user
 | 
			
		||||
- ✅ **Graceful exit handling**: ESC key in edit entry mode now triggers save confirmation instead of auto-exiting
 | 
			
		||||
- ✅ **Validation integration**: Full validation before saving with clear error messages for invalid data
 | 
			
		||||
- ✅ **Comprehensive testing**: 13 new tests for save confirmation functionality (161 total tests)
 | 
			
		||||
- ✅ **Modal keyboard shortcuts**: Save (S), Discard (D), Cancel (ESC) with intuitive button labels
 | 
			
		||||
 | 
			
		||||
### Phase 2 Implementation Complete
 | 
			
		||||
- ✅ **Advanced configuration system**: Complete Config class with JSON persistence to ~/.config/hosts-manager/
 | 
			
		||||
- ✅ **Professional configuration modal**: Modal dialog with keyboard bindings for settings management
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,7 +64,9 @@
 | 
			
		|||
- ✅ **Live testing**: Manual testing confirms all functionality works correctly
 | 
			
		||||
- ✅ **Human-readable formatting**: Tab-based column alignment with proper spacing
 | 
			
		||||
- ✅ **Management header**: Automatic addition of management header to hosts files
 | 
			
		||||
- ✅ **Auto-save functionality**: Immediate saving of changes when entries are edited
 | 
			
		||||
- ✅ **Save confirmation modal**: Professional modal dialog for save/discard/cancel when exiting edit entry mode
 | 
			
		||||
- ✅ **Change detection**: Intelligent tracking of original vs. current entry values
 | 
			
		||||
- ✅ **No auto-save**: Changes saved only when explicitly confirmed by user
 | 
			
		||||
 | 
			
		||||
### Phase 4: Advanced Edit Features
 | 
			
		||||
- ❌ **Add new entries**: Create new host entries
 | 
			
		||||
| 
						 | 
				
			
			@ -89,8 +91,8 @@
 | 
			
		|||
## Current Status
 | 
			
		||||
 | 
			
		||||
### Development Stage
 | 
			
		||||
**Stage**: Phase 3 Complete - Moving to Phase 4
 | 
			
		||||
**Progress**: 75% (Complete edit mode foundation with permission management)
 | 
			
		||||
**Stage**: Phase 3 Complete with Save Confirmation Enhancement - Ready for Phase 4
 | 
			
		||||
**Progress**: 78% (Complete edit mode foundation with professional save confirmation)
 | 
			
		||||
**Next Milestone**: Advanced edit features (add/delete entries, bulk operations)
 | 
			
		||||
 | 
			
		||||
### Phase 2 Final Achievements
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +114,10 @@
 | 
			
		|||
6. ✅ **Manager module**: Complete HostsManager class for all edit operations
 | 
			
		||||
7. ✅ **Safe file operations**: Atomic file writing with rollback capability
 | 
			
		||||
8. ✅ **Enhanced error messages**: Professional read-only mode error messages with clear instructions
 | 
			
		||||
9. ✅ **Comprehensive testing**: 38 new tests for manager module (135 total tests)
 | 
			
		||||
9. ✅ **Comprehensive testing**: 38 new tests for manager module (148 total tests)
 | 
			
		||||
10. ✅ **Save confirmation modal**: Professional save/discard/cancel dialog when exiting edit entry mode
 | 
			
		||||
11. ✅ **Change detection system**: Intelligent tracking of original vs. current entry values
 | 
			
		||||
12. ✅ **No auto-save behavior**: User-controlled saving with explicit confirmation
 | 
			
		||||
 | 
			
		||||
### Recent Major Accomplishments
 | 
			
		||||
- ✅ **Complete Phase 3 implementation**: Full edit mode foundation with permission management
 | 
			
		||||
| 
						 | 
				
			
			@ -121,7 +126,8 @@
 | 
			
		|||
- ✅ **Permission system**: Robust sudo request, validation, and release functionality
 | 
			
		||||
- ✅ **File backup system**: Automatic backup creation with timestamp naming
 | 
			
		||||
- ✅ **Entry manipulation**: Toggle active/inactive and reorder entries safely
 | 
			
		||||
- ✅ **Comprehensive testing**: 135 total tests with 100% pass rate including edit operations
 | 
			
		||||
- ✅ **Save confirmation enhancement**: Professional modal dialog system for editing workflow
 | 
			
		||||
- ✅ **Comprehensive testing**: 161 total tests with 100% pass rate including save confirmation
 | 
			
		||||
- ✅ **Error handling**: Graceful handling of permission errors and file operations
 | 
			
		||||
- ✅ **Keyboard shortcuts**: Complete set of edit mode bindings (e, space, Ctrl+Up/Down, Ctrl+S)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										112
									
								
								src/hosts/tui/save_confirmation_modal.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/hosts/tui/save_confirmation_modal.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,112 @@
 | 
			
		|||
"""
 | 
			
		||||
Save confirmation modal for the hosts TUI application.
 | 
			
		||||
 | 
			
		||||
This module provides a modal dialog to confirm saving changes when exiting edit entry mode.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from textual.app import ComposeResult
 | 
			
		||||
from textual.containers import Vertical, Horizontal
 | 
			
		||||
from textual.widgets import Static, Button, Label
 | 
			
		||||
from textual.screen import ModalScreen
 | 
			
		||||
from textual.binding import Binding
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SaveConfirmationModal(ModalScreen):
 | 
			
		||||
    """
 | 
			
		||||
    Modal screen for save confirmation when exiting edit entry mode.
 | 
			
		||||
 | 
			
		||||
    Provides a confirmation dialog asking whether to save or discard changes.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    CSS = """
 | 
			
		||||
    SaveConfirmationModal {
 | 
			
		||||
        align: center middle;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .save-confirmation-container {
 | 
			
		||||
        width: 60;
 | 
			
		||||
        height: 12;
 | 
			
		||||
        background: $surface;
 | 
			
		||||
        border: thick $primary;
 | 
			
		||||
        padding: 1;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .save-confirmation-title {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        text-style: bold;
 | 
			
		||||
        color: $primary;
 | 
			
		||||
        margin-bottom: 1;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .save-confirmation-message {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        margin-bottom: 2;
 | 
			
		||||
        color: $text;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .button-row {
 | 
			
		||||
        align: center middle;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .save-confirmation-button {
 | 
			
		||||
        margin: 0 1;
 | 
			
		||||
        min-width: 12;
 | 
			
		||||
    }
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    BINDINGS = [
 | 
			
		||||
        Binding("escape", "cancel", "Cancel"),
 | 
			
		||||
        Binding("enter", "save", "Save"),
 | 
			
		||||
        Binding("s", "save", "Save"),
 | 
			
		||||
        Binding("d", "discard", "Discard"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def compose(self) -> ComposeResult:
 | 
			
		||||
        """Create the save confirmation modal layout."""
 | 
			
		||||
        with Vertical(classes="save-confirmation-container"):
 | 
			
		||||
            yield Static("Save Changes?", classes="save-confirmation-title")
 | 
			
		||||
            yield Label(
 | 
			
		||||
                "You have made changes to this entry.\nDo you want to save or discard them?",
 | 
			
		||||
                classes="save-confirmation-message",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            with Horizontal(classes="button-row"):
 | 
			
		||||
                yield Button(
 | 
			
		||||
                    "Save (S)",
 | 
			
		||||
                    variant="primary",
 | 
			
		||||
                    id="save-button",
 | 
			
		||||
                    classes="save-confirmation-button",
 | 
			
		||||
                )
 | 
			
		||||
                yield Button(
 | 
			
		||||
                    "Discard (D)",
 | 
			
		||||
                    variant="default",
 | 
			
		||||
                    id="discard-button",
 | 
			
		||||
                    classes="save-confirmation-button",
 | 
			
		||||
                )
 | 
			
		||||
                yield Button(
 | 
			
		||||
                    "Cancel (ESC)",
 | 
			
		||||
                    variant="default",
 | 
			
		||||
                    id="cancel-button",
 | 
			
		||||
                    classes="save-confirmation-button",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    def on_button_pressed(self, event: Button.Pressed) -> None:
 | 
			
		||||
        """Handle button presses."""
 | 
			
		||||
        if event.button.id == "save-button":
 | 
			
		||||
            self.action_save()
 | 
			
		||||
        elif event.button.id == "discard-button":
 | 
			
		||||
            self.action_discard()
 | 
			
		||||
        elif event.button.id == "cancel-button":
 | 
			
		||||
            self.action_cancel()
 | 
			
		||||
 | 
			
		||||
    def action_save(self) -> None:
 | 
			
		||||
        """Save changes and close modal."""
 | 
			
		||||
        self.dismiss("save")
 | 
			
		||||
 | 
			
		||||
    def action_discard(self) -> None:
 | 
			
		||||
        """Discard changes and close modal."""
 | 
			
		||||
        self.dismiss("discard")
 | 
			
		||||
 | 
			
		||||
    def action_cancel(self) -> None:
 | 
			
		||||
        """Cancel operation and close modal."""
 | 
			
		||||
        self.dismiss("cancel")
 | 
			
		||||
							
								
								
									
										284
									
								
								tests/test_save_confirmation_modal.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								tests/test_save_confirmation_modal.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,284 @@
 | 
			
		|||
"""
 | 
			
		||||
Tests for the save confirmation modal.
 | 
			
		||||
 | 
			
		||||
This module tests the save confirmation functionality when exiting edit entry mode.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
from unittest.mock import Mock, patch
 | 
			
		||||
from textual.widgets import Input, Checkbox
 | 
			
		||||
 | 
			
		||||
from hosts.main import HostsManagerApp
 | 
			
		||||
from hosts.core.models import HostsFile, HostEntry
 | 
			
		||||
from hosts.tui.save_confirmation_modal import SaveConfirmationModal
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSaveConfirmationModal:
 | 
			
		||||
    """Test cases for the SaveConfirmationModal class."""
 | 
			
		||||
 | 
			
		||||
    def test_modal_creation(self):
 | 
			
		||||
        """Test that the modal can be created."""
 | 
			
		||||
        modal = SaveConfirmationModal()
 | 
			
		||||
        assert modal is not None
 | 
			
		||||
 | 
			
		||||
    def test_modal_compose(self):
 | 
			
		||||
        """Test that the modal composes correctly."""
 | 
			
		||||
        # Note: Cannot test compose() directly without app context
 | 
			
		||||
        # This is a basic existence check for the modal
 | 
			
		||||
        modal = SaveConfirmationModal()
 | 
			
		||||
        assert hasattr(modal, "compose")
 | 
			
		||||
        assert callable(modal.compose)
 | 
			
		||||
 | 
			
		||||
    def test_action_save(self):
 | 
			
		||||
        """Test save action dismisses with 'save'."""
 | 
			
		||||
        modal = SaveConfirmationModal()
 | 
			
		||||
        modal.dismiss = Mock()
 | 
			
		||||
 | 
			
		||||
        modal.action_save()
 | 
			
		||||
 | 
			
		||||
        modal.dismiss.assert_called_once_with("save")
 | 
			
		||||
 | 
			
		||||
    def test_action_discard(self):
 | 
			
		||||
        """Test discard action dismisses with 'discard'."""
 | 
			
		||||
        modal = SaveConfirmationModal()
 | 
			
		||||
        modal.dismiss = Mock()
 | 
			
		||||
 | 
			
		||||
        modal.action_discard()
 | 
			
		||||
 | 
			
		||||
        modal.dismiss.assert_called_once_with("discard")
 | 
			
		||||
 | 
			
		||||
    def test_action_cancel(self):
 | 
			
		||||
        """Test cancel action dismisses with 'cancel'."""
 | 
			
		||||
        modal = SaveConfirmationModal()
 | 
			
		||||
        modal.dismiss = Mock()
 | 
			
		||||
 | 
			
		||||
        modal.action_cancel()
 | 
			
		||||
 | 
			
		||||
        modal.dismiss.assert_called_once_with("cancel")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSaveConfirmationIntegration:
 | 
			
		||||
    """Test cases for save confirmation integration with the main app."""
 | 
			
		||||
 | 
			
		||||
    @pytest.fixture
 | 
			
		||||
    def app(self):
 | 
			
		||||
        """Create a test app instance."""
 | 
			
		||||
        return HostsManagerApp()
 | 
			
		||||
 | 
			
		||||
    def test_has_entry_changes_no_original_values(self, app):
 | 
			
		||||
        """Test has_entry_changes returns False when no original values stored."""
 | 
			
		||||
        app.original_entry_values = None
 | 
			
		||||
        app.entry_edit_mode = True
 | 
			
		||||
 | 
			
		||||
        assert not app.has_entry_changes()
 | 
			
		||||
 | 
			
		||||
    def test_has_entry_changes_not_in_edit_mode(self, app):
 | 
			
		||||
        """Test has_entry_changes returns False when not in edit mode."""
 | 
			
		||||
        app.original_entry_values = {
 | 
			
		||||
            "ip_address": "127.0.0.1",
 | 
			
		||||
            "hostnames": ["localhost"],
 | 
			
		||||
            "comment": None,
 | 
			
		||||
            "is_active": True,
 | 
			
		||||
        }
 | 
			
		||||
        app.entry_edit_mode = False
 | 
			
		||||
 | 
			
		||||
        assert not app.has_entry_changes()
 | 
			
		||||
 | 
			
		||||
    @patch.object(HostsManagerApp, "query_one")
 | 
			
		||||
    def test_has_entry_changes_no_changes(self, mock_query_one, app):
 | 
			
		||||
        """Test has_entry_changes returns False when no changes made."""
 | 
			
		||||
        # Setup original values
 | 
			
		||||
        app.original_entry_values = {
 | 
			
		||||
            "ip_address": "127.0.0.1",
 | 
			
		||||
            "hostnames": ["localhost"],
 | 
			
		||||
            "comment": None,
 | 
			
		||||
            "is_active": True,
 | 
			
		||||
        }
 | 
			
		||||
        app.entry_edit_mode = True
 | 
			
		||||
 | 
			
		||||
        # Mock form fields with original values
 | 
			
		||||
        mock_ip_input = Mock()
 | 
			
		||||
        mock_ip_input.value = "127.0.0.1"
 | 
			
		||||
        mock_hostname_input = Mock()
 | 
			
		||||
        mock_hostname_input.value = "localhost"
 | 
			
		||||
        mock_comment_input = Mock()
 | 
			
		||||
        mock_comment_input.value = ""
 | 
			
		||||
        mock_checkbox = Mock()
 | 
			
		||||
        mock_checkbox.value = True
 | 
			
		||||
 | 
			
		||||
        def mock_query_side_effect(selector, widget_type=None):
 | 
			
		||||
            if selector == "#ip-input":
 | 
			
		||||
                return mock_ip_input
 | 
			
		||||
            elif selector == "#hostname-input":
 | 
			
		||||
                return mock_hostname_input
 | 
			
		||||
            elif selector == "#comment-input":
 | 
			
		||||
                return mock_comment_input
 | 
			
		||||
            elif selector == "#active-checkbox":
 | 
			
		||||
                return mock_checkbox
 | 
			
		||||
 | 
			
		||||
        mock_query_one.side_effect = mock_query_side_effect
 | 
			
		||||
 | 
			
		||||
        assert not app.has_entry_changes()
 | 
			
		||||
 | 
			
		||||
    @patch.object(HostsManagerApp, "query_one")
 | 
			
		||||
    def test_has_entry_changes_ip_changed(self, mock_query_one, app):
 | 
			
		||||
        """Test has_entry_changes returns True when IP address changed."""
 | 
			
		||||
        # Setup original values
 | 
			
		||||
        app.original_entry_values = {
 | 
			
		||||
            "ip_address": "127.0.0.1",
 | 
			
		||||
            "hostnames": ["localhost"],
 | 
			
		||||
            "comment": None,
 | 
			
		||||
            "is_active": True,
 | 
			
		||||
        }
 | 
			
		||||
        app.entry_edit_mode = True
 | 
			
		||||
 | 
			
		||||
        # Mock form fields with changed IP
 | 
			
		||||
        mock_ip_input = Mock()
 | 
			
		||||
        mock_ip_input.value = "192.168.1.1"  # Changed IP
 | 
			
		||||
        mock_hostname_input = Mock()
 | 
			
		||||
        mock_hostname_input.value = "localhost"
 | 
			
		||||
        mock_comment_input = Mock()
 | 
			
		||||
        mock_comment_input.value = ""
 | 
			
		||||
        mock_checkbox = Mock()
 | 
			
		||||
        mock_checkbox.value = True
 | 
			
		||||
 | 
			
		||||
        def mock_query_side_effect(selector, widget_type=None):
 | 
			
		||||
            if selector == "#ip-input":
 | 
			
		||||
                return mock_ip_input
 | 
			
		||||
            elif selector == "#hostname-input":
 | 
			
		||||
                return mock_hostname_input
 | 
			
		||||
            elif selector == "#comment-input":
 | 
			
		||||
                return mock_comment_input
 | 
			
		||||
            elif selector == "#active-checkbox":
 | 
			
		||||
                return mock_checkbox
 | 
			
		||||
 | 
			
		||||
        mock_query_one.side_effect = mock_query_side_effect
 | 
			
		||||
 | 
			
		||||
        assert app.has_entry_changes()
 | 
			
		||||
 | 
			
		||||
    @patch.object(HostsManagerApp, "query_one")
 | 
			
		||||
    def test_has_entry_changes_hostname_changed(self, mock_query_one, app):
 | 
			
		||||
        """Test has_entry_changes returns True when hostname changed."""
 | 
			
		||||
        # Setup original values
 | 
			
		||||
        app.original_entry_values = {
 | 
			
		||||
            "ip_address": "127.0.0.1",
 | 
			
		||||
            "hostnames": ["localhost"],
 | 
			
		||||
            "comment": None,
 | 
			
		||||
            "is_active": True,
 | 
			
		||||
        }
 | 
			
		||||
        app.entry_edit_mode = True
 | 
			
		||||
 | 
			
		||||
        # Mock form fields with changed hostname
 | 
			
		||||
        mock_ip_input = Mock()
 | 
			
		||||
        mock_ip_input.value = "127.0.0.1"
 | 
			
		||||
        mock_hostname_input = Mock()
 | 
			
		||||
        mock_hostname_input.value = "localhost, test.local"  # Added hostname
 | 
			
		||||
        mock_comment_input = Mock()
 | 
			
		||||
        mock_comment_input.value = ""
 | 
			
		||||
        mock_checkbox = Mock()
 | 
			
		||||
        mock_checkbox.value = True
 | 
			
		||||
 | 
			
		||||
        def mock_query_side_effect(selector, widget_type=None):
 | 
			
		||||
            if selector == "#ip-input":
 | 
			
		||||
                return mock_ip_input
 | 
			
		||||
            elif selector == "#hostname-input":
 | 
			
		||||
                return mock_hostname_input
 | 
			
		||||
            elif selector == "#comment-input":
 | 
			
		||||
                return mock_comment_input
 | 
			
		||||
            elif selector == "#active-checkbox":
 | 
			
		||||
                return mock_checkbox
 | 
			
		||||
 | 
			
		||||
        mock_query_one.side_effect = mock_query_side_effect
 | 
			
		||||
 | 
			
		||||
        assert app.has_entry_changes()
 | 
			
		||||
 | 
			
		||||
    @patch.object(HostsManagerApp, "query_one")
 | 
			
		||||
    def test_has_entry_changes_comment_added(self, mock_query_one, app):
 | 
			
		||||
        """Test has_entry_changes returns True when comment added."""
 | 
			
		||||
        # Setup original values
 | 
			
		||||
        app.original_entry_values = {
 | 
			
		||||
            "ip_address": "127.0.0.1",
 | 
			
		||||
            "hostnames": ["localhost"],
 | 
			
		||||
            "comment": None,
 | 
			
		||||
            "is_active": True,
 | 
			
		||||
        }
 | 
			
		||||
        app.entry_edit_mode = True
 | 
			
		||||
 | 
			
		||||
        # Mock form fields with added comment
 | 
			
		||||
        mock_ip_input = Mock()
 | 
			
		||||
        mock_ip_input.value = "127.0.0.1"
 | 
			
		||||
        mock_hostname_input = Mock()
 | 
			
		||||
        mock_hostname_input.value = "localhost"
 | 
			
		||||
        mock_comment_input = Mock()
 | 
			
		||||
        mock_comment_input.value = "Test comment"  # Added comment
 | 
			
		||||
        mock_checkbox = Mock()
 | 
			
		||||
        mock_checkbox.value = True
 | 
			
		||||
 | 
			
		||||
        def mock_query_side_effect(selector, widget_type=None):
 | 
			
		||||
            if selector == "#ip-input":
 | 
			
		||||
                return mock_ip_input
 | 
			
		||||
            elif selector == "#hostname-input":
 | 
			
		||||
                return mock_hostname_input
 | 
			
		||||
            elif selector == "#comment-input":
 | 
			
		||||
                return mock_comment_input
 | 
			
		||||
            elif selector == "#active-checkbox":
 | 
			
		||||
                return mock_checkbox
 | 
			
		||||
 | 
			
		||||
        mock_query_one.side_effect = mock_query_side_effect
 | 
			
		||||
 | 
			
		||||
        assert app.has_entry_changes()
 | 
			
		||||
 | 
			
		||||
    @patch.object(HostsManagerApp, "query_one")
 | 
			
		||||
    def test_has_entry_changes_active_state_changed(self, mock_query_one, app):
 | 
			
		||||
        """Test has_entry_changes returns True when active state changed."""
 | 
			
		||||
        # Setup original values
 | 
			
		||||
        app.original_entry_values = {
 | 
			
		||||
            "ip_address": "127.0.0.1",
 | 
			
		||||
            "hostnames": ["localhost"],
 | 
			
		||||
            "comment": None,
 | 
			
		||||
            "is_active": True,
 | 
			
		||||
        }
 | 
			
		||||
        app.entry_edit_mode = True
 | 
			
		||||
 | 
			
		||||
        # Mock form fields with changed active state
 | 
			
		||||
        mock_ip_input = Mock()
 | 
			
		||||
        mock_ip_input.value = "127.0.0.1"
 | 
			
		||||
        mock_hostname_input = Mock()
 | 
			
		||||
        mock_hostname_input.value = "localhost"
 | 
			
		||||
        mock_comment_input = Mock()
 | 
			
		||||
        mock_comment_input.value = ""
 | 
			
		||||
        mock_checkbox = Mock()
 | 
			
		||||
        mock_checkbox.value = False  # Changed active state
 | 
			
		||||
 | 
			
		||||
        def mock_query_side_effect(selector, widget_type=None):
 | 
			
		||||
            if selector == "#ip-input":
 | 
			
		||||
                return mock_ip_input
 | 
			
		||||
            elif selector == "#hostname-input":
 | 
			
		||||
                return mock_hostname_input
 | 
			
		||||
            elif selector == "#comment-input":
 | 
			
		||||
                return mock_comment_input
 | 
			
		||||
            elif selector == "#active-checkbox":
 | 
			
		||||
                return mock_checkbox
 | 
			
		||||
 | 
			
		||||
        mock_query_one.side_effect = mock_query_side_effect
 | 
			
		||||
 | 
			
		||||
        assert app.has_entry_changes()
 | 
			
		||||
 | 
			
		||||
    def test_exit_edit_entry_mode(self, app):
 | 
			
		||||
        """Test exit_edit_entry_mode cleans up properly."""
 | 
			
		||||
        app.entry_edit_mode = True
 | 
			
		||||
        app.original_entry_values = {"test": "data"}
 | 
			
		||||
        app.update_entry_details = Mock()
 | 
			
		||||
        app.query_one = Mock()
 | 
			
		||||
        app.update_status = Mock()
 | 
			
		||||
 | 
			
		||||
        mock_table = Mock()
 | 
			
		||||
        app.query_one.return_value = mock_table
 | 
			
		||||
 | 
			
		||||
        app.exit_edit_entry_mode()
 | 
			
		||||
 | 
			
		||||
        assert not app.entry_edit_mode
 | 
			
		||||
        assert app.original_entry_values is None
 | 
			
		||||
        app.update_entry_details.assert_called_once()
 | 
			
		||||
        mock_table.focus.assert_called_once()
 | 
			
		||||
        app.update_status.assert_called_once_with("Exited entry edit mode")
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue