Compare commits
No commits in common. "220818c8d159a3ffc79b472f21bb9fbed2ece12d" and "116752135566e7732db1907ed7fdff159b5208db" have entirely different histories.
220818c8d1
...
1167521355
9 changed files with 156 additions and 291 deletions
|
@ -1,44 +1,19 @@
|
||||||
# Ac### Status Appearance Enhancement ✅ COMPLETED
|
# Active Context: hosts
|
||||||
Successfully implemented the user's requested status display improvements with overlay fix:
|
|
||||||
|
|
||||||
**New Header Layout:**
|
|
||||||
- **Title**: Changed from "Hosts Manager" to "/etc/hosts Manager"
|
|
||||||
- **Subtitle**: Now shows "29 entries (6 active) | Read-only mode" format
|
|
||||||
- **Error Messages**: Moved to dedicated status bar below header as overlay
|
|
||||||
|
|
||||||
**Overlay Status Bar Implementation:**
|
|
||||||
- **Fixed layout shifting issue**: Status bar now appears as overlay without moving panes down
|
|
||||||
- **Corrected positioning**: Status bar appears below header as overlay using `dock: top`, `layer: overlay`, `offset-y: 3`
|
|
||||||
- **Visible error messages**: Error messages now display correctly as overlay on content area
|
|
||||||
- **No layout flow impact**: Panes stay in exact same position when error messages appear
|
|
||||||
- **Professional appearance**: Error bar overlays cleanly below header without disrupting content layout
|
|
||||||
|
|
||||||
**Implementation Details:**
|
|
||||||
- Moved status bar widget to end of compose method for overlay rendering
|
|
||||||
- Status bar positioned 3 lines down from top (below header) using CSS offset
|
|
||||||
- Status bar is hidden by default, only appears when displaying messages
|
|
||||||
- Error messages (❌) auto-clear after 5 seconds, regular messages after 3 seconds
|
|
||||||
- Header subtitle always shows current status regardless of temporary messages
|
|
||||||
|
|
||||||
**Test Updates:**
|
|
||||||
- All 149 tests passing with overlay status bar implementation
|
|
||||||
- Fixed layout shifting that was annoying when error messages appeared
|
|
||||||
- Verified functionality maintains all previous behaviorive Context: hosts
|
|
||||||
|
|
||||||
## Current Work Focus
|
## Current Work Focus
|
||||||
|
|
||||||
**Status Appearance Enhancement Complete**: Successfully implemented the user's requested status display improvements. The header now shows "/etc/hosts Manager" with entry counts and mode on the right, while error messages appear in a dedicated status bar below the header. Ready to proceed with remaining UX improvements from todo.md.
|
**Post-Phase 3 Code Quality Maintenance**: The hosts TUI application has successfully completed Phase 3 with full edit mode foundation, save confirmation functionality, and comprehensive testing (149 tests). However, 20 minor linting issues (unused imports and variables) require cleanup before proceeding to Phase 4 advanced features.
|
||||||
|
|
||||||
## Immediate Next Steps
|
## Immediate Next Steps
|
||||||
|
|
||||||
### Priority 1: Remaining User Experience Improvements (From todo.md)
|
### Priority 1: Code Quality Cleanup
|
||||||
1. ✅ **Status appearance enhancement**: COMPLETED - New header layout with separate error message bar
|
1. **Fix linting issues**: Run `uv run ruff check --fix` to address 20 unused import and variable warnings
|
||||||
2. ✅ **Entry details consistency**: COMPLETED - DataTable with labeled rows matching edit form order
|
2. **Validate fixes**: Ensure all tests still pass (149 tests) after cleanup
|
||||||
3. ❌ **DataTable details implementation**: COMPLETED as part of entry details consistency
|
3. **Confirm application functionality**: Test that `uv run hosts` still works perfectly
|
||||||
4. ❌ **Sudo permission fixes**: Address known sudo handling issues
|
4. **Commit clean state**: Create commit with "Fix linting issues" once cleanup is complete
|
||||||
|
|
||||||
### Priority 2: Phase 4 Planning
|
### Priority 2: Phase 4 Planning
|
||||||
Once remaining UX improvements are complete:
|
Once code quality is restored:
|
||||||
1. **Advanced entry operations**: Add/delete entries with validation
|
1. **Advanced entry operations**: Add/delete entries with validation
|
||||||
2. **Search functionality**: Find entries by hostname or IP address
|
2. **Search functionality**: Find entries by hostname or IP address
|
||||||
3. **Bulk operations**: Select and modify multiple entries
|
3. **Bulk operations**: Select and modify multiple entries
|
||||||
|
@ -47,26 +22,27 @@ Once remaining UX improvements are complete:
|
||||||
## Memory Bank Update Summary
|
## Memory Bank Update Summary
|
||||||
|
|
||||||
### Files Updated
|
### Files Updated
|
||||||
- ✅ **activeContext.md**: Updated current focus to test stabilization and UX improvements
|
- ✅ **activeContext.md**: Updated current focus and next steps
|
||||||
- ✅ **progress.md**: Corrected test status (8 failures out of 149) and development stage
|
- ✅ **progress.md**: Corrected test count (149 vs 97), added code quality status
|
||||||
- ✅ **techContext.md**: Updated development workflow and test status
|
- ✅ **techContext.md**: Updated development workflow and code quality status
|
||||||
- ✅ **projectbrief.md**: Noted current test failures in testing strategy
|
- ✅ **systemPatterns.md**: Added edit mode and permission management patterns
|
||||||
- ✅ **Added todo.md insights**: Documented user experience improvement requirements
|
- ✅ **projectbrief.md**: Updated test coverage details and current status
|
||||||
|
|
||||||
### Current Status Corrections
|
### Key Corrections Made
|
||||||
- **Linting status**: Corrected to show clean state (all checks passing)
|
- **Test count**: Updated from 97 to 149 tests across all files
|
||||||
- **Test status**: Updated to reflect 8 failing tests out of 149 total
|
- **Code quality**: Noted 20 linting issues requiring cleanup
|
||||||
- **Application functionality**: Confirmed working TUI with identified improvement areas
|
- **Project stage**: Clarified completion of Phase 3 with save confirmation
|
||||||
- **Development priority**: Shifted from code cleanup to test stabilization
|
- **Current status**: Maintenance phase before Phase 4 development
|
||||||
- **User requirements**: Added todo.md requirements for status, details, and sudo improvements
|
- **Recent commits**: Reflected completion of save confirmation modal
|
||||||
|
|
||||||
### New Requirements from todo.md
|
### Architecture Insights Confirmed
|
||||||
1. **Status appearance enhancement**: Visual design improvements needed
|
- **Textual framework**: Excellent for complex TUI applications with modal dialogs
|
||||||
2. **Entry details consistency**: Non-edit view should match edit mode field order
|
- **Layered architecture**: Proven effective for maintainable, testable code
|
||||||
3. **DataTable details view**: Implement labeled rows for better presentation
|
- **Test-driven development**: 149 comprehensive tests enable confident refactoring
|
||||||
4. **Sudo issue resolution**: Address known permission handling problems
|
- **Configuration system**: JSON-based persistence working reliably
|
||||||
|
- **Permission management**: Sudo handling implemented safely and securely
|
||||||
|
|
||||||
The memory bank now accurately reflects the true current state: a functional application with clean code but test stability issues and identified user experience improvements needed before Phase 4 development.
|
The memory bank now accurately reflects the current state of the project, ready for the next phase of development after code quality maintenance.
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
|
||||||
|
@ -95,45 +71,22 @@ The memory bank now accurately reflects the true current state: a functional app
|
||||||
- **Professional visual design**: Color-coded entries, zebra striping, and rich text styling
|
- **Professional visual design**: Color-coded entries, zebra striping, and rich text styling
|
||||||
- **Interactive sorting**: Click column headers or use keyboard shortcuts to sort data
|
- **Interactive sorting**: Click column headers or use keyboard shortcuts to sort data
|
||||||
- **Intelligent filtering**: Hide default system entries based on user preference
|
- **Intelligent filtering**: Hide default system entries based on user preference
|
||||||
- **Comprehensive test coverage**: All 149 tests passing with 100% success rate
|
- **Comprehensive test coverage**: 149 tests with 100% pass rate covering all components
|
||||||
- **Clean code quality**: All ruff linting and formatting checks passing
|
- **Code quality maintenance needed**: 20 linting issues (unused imports/variables) require cleanup
|
||||||
- **Robust architecture**: Clean layered design ready for UX improvements and Phase 4 features
|
- **Robust architecture**: Clean layered design ready for Phase 4 advanced features
|
||||||
- **Todo requirements identified**: Status appearance, entry details consistency, sudo handling improvements needed
|
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
### Entry Details Consistency ✅ COMPLETED
|
### Immediate Priority: Code Quality Cleanup
|
||||||
Successfully implemented DataTable-based entry details with consistent field ordering:
|
1. **Fix linting issues**: Address 20 unused import and variable warnings
|
||||||
|
- Remove unused imports in core/config.py, test files
|
||||||
|
- Clean up unused variables in exception handling
|
||||||
|
- Run `uv run ruff check --fix` to auto-fix issues
|
||||||
|
|
||||||
**Key Improvements:**
|
2. **Code quality validation**:
|
||||||
- **Replaced Static widget with DataTable**: Entry details now displayed in professional table format
|
- Ensure all ruff checks pass with zero issues
|
||||||
- **Consistent field order**: Details view now matches edit form order exactly
|
- Maintain perfect test coverage (149 tests passing)
|
||||||
1. IP Address
|
- Verify application functionality after cleanup
|
||||||
2. Hostnames (comma-separated)
|
|
||||||
3. Comment
|
|
||||||
4. Active status (Yes/No)
|
|
||||||
- **Labeled rows**: Uses DataTable labeled rows feature for clean presentation
|
|
||||||
- **No headers**: DataTable configured with `show_header=False` for clean appearance
|
|
||||||
|
|
||||||
**Implementation Details:**
|
|
||||||
- Modified `app.py` compose method to use DataTable instead of Static widget
|
|
||||||
- Updated `details_handler.py` to populate DataTable with labeled rows
|
|
||||||
- Added CSS styling for entry details table consistency
|
|
||||||
- Fixed 2 failing tests to work with new DataTable approach
|
|
||||||
- All 149 tests passing with new implementation
|
|
||||||
|
|
||||||
**Visual Benefits:**
|
|
||||||
- Professional table appearance matching main entries table
|
|
||||||
- Clear field labels in left column, values in right column
|
|
||||||
- Proper spacing and alignment
|
|
||||||
- System default entry warnings displayed in table format
|
|
||||||
- DNS Name field shown when present (read-only information)
|
|
||||||
|
|
||||||
### Priority 2: User Experience Improvements (From todo.md)
|
|
||||||
1. **Status appearance enhancement**: Improve visual design of status bar
|
|
||||||
2. **Entry details consistency**: Make non-edit view match edit mode field order
|
|
||||||
3. **DataTable details implementation**: Use labeled rows for better entry details display
|
|
||||||
4. **Sudo permission fixes**: Address known sudo handling issues
|
|
||||||
|
|
||||||
### Phase 4: Advanced Edit Features (Next Phase)
|
### Phase 4: Advanced Edit Features (Next Phase)
|
||||||
1. **Advanced editing operations**:
|
1. **Advanced editing operations**:
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
- ✅ **Entry management**: DataTable with proper formatting and status indicators
|
- ✅ **Entry management**: DataTable with proper formatting and status indicators
|
||||||
- ✅ **Detail view**: Comprehensive entry details in right pane
|
- ✅ **Detail view**: Comprehensive entry details in right pane
|
||||||
- ✅ **Navigation**: Smooth keyboard navigation with cursor position restoration
|
- ✅ **Navigation**: Smooth keyboard navigation with cursor position restoration
|
||||||
- ✅ **Testing**: 149 comprehensive tests with 100% pass rate (test stabilization completed)
|
- ✅ **Testing**: 97 comprehensive tests with 100% pass rate
|
||||||
- ✅ **Code quality**: All ruff linting and formatting checks passing
|
- ✅ **Code quality**: All ruff linting and formatting checks passing
|
||||||
- ✅ **Error handling**: Graceful handling of file access and parsing errors
|
- ✅ **Error handling**: Graceful handling of file access and parsing errors
|
||||||
- ✅ **Status feedback**: Informative status bar with file and entry information
|
- ✅ **Status feedback**: Informative status bar with file and entry information
|
||||||
|
@ -67,13 +67,12 @@
|
||||||
|
|
||||||
## What's Left to Build
|
## What's Left to Build
|
||||||
|
|
||||||
### Priority 1: User Experience Improvements (From todo.md)
|
### Immediate Priority: Code Quality Cleanup
|
||||||
- ✅ **Status appearance**: Enhanced visual design with new header layout and dedicated error message bar
|
- ❌ **Fix linting issues**: Address 20 unused import and variable warnings
|
||||||
### Priority 1: User Experience Improvements (From todo.md)
|
- ❌ **Code quality validation**: Ensure all ruff checks pass with zero issues
|
||||||
- ✅ **Status appearance**: Enhanced visual design with new header layout and separate error message bar
|
- ❌ **Maintain test coverage**: Keep 149 tests passing during cleanup
|
||||||
- ✅ **Entry details consistency**: Implemented DataTable with labeled rows matching edit form field order
|
|
||||||
- ✅ **DataTable details view**: Completed as part of entry details consistency improvement
|
### Phase 4: Advanced Edit Features
|
||||||
- ❌ **Sudo permission handling**: Address known sudo issues### Phase 4: Advanced Edit Features
|
|
||||||
- ❌ **Add new entries**: Create new host entries
|
- ❌ **Add new entries**: Create new host entries
|
||||||
- ❌ **Delete entries**: Remove host entries
|
- ❌ **Delete entries**: Remove host entries
|
||||||
- ❌ **Bulk operations**: Select and modify multiple entries
|
- ❌ **Bulk operations**: Select and modify multiple entries
|
||||||
|
@ -96,10 +95,9 @@
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
### Development Stage
|
### Development Stage
|
||||||
**Stage**: User Experience Improvements - 3 of 4 Todo Items Complete
|
**Stage**: Phase 3 Complete with Code Quality Maintenance Required
|
||||||
**Progress**: 90% (Status improvements and entry details consistency completed, ready for final sudo fixes and Phase 4)
|
**Progress**: 82% (Complete edit mode foundation with save confirmation, code cleanup needed)
|
||||||
**Next Milestone**: Sudo permission handling fixes, then Phase 4 advanced features
|
**Next Milestone**: Code quality cleanup, then Phase 4 advanced edit features
|
||||||
**Test Status**: ✅ All 149 tests passing (maintained during UX improvements)
|
|
||||||
|
|
||||||
### Phase 3 Final Achievements ✅ COMPLETE
|
### Phase 3 Final Achievements ✅ COMPLETE
|
||||||
1. ✅ **Permission management**: Complete PermissionManager class with sudo request and validation
|
1. ✅ **Permission management**: Complete PermissionManager class with sudo request and validation
|
||||||
|
|
|
@ -81,7 +81,7 @@ hosts/
|
||||||
- Mock `/etc/hosts` file I/O and DNS lookups to avoid system dependencies.
|
- Mock `/etc/hosts` file I/O and DNS lookups to avoid system dependencies.
|
||||||
- Include integration tests for the Textual TUI (using `textual.testing` or snapshot testing).
|
- Include integration tests for the Textual TUI (using `textual.testing` or snapshot testing).
|
||||||
|
|
||||||
### Implemented Tests (149 tests total, 8 failing)
|
### Implemented Tests (149 tests total)
|
||||||
|
|
||||||
1. **Parsing Tests** (15 tests):
|
1. **Parsing Tests** (15 tests):
|
||||||
- Parse simple `/etc/hosts` with comments and disabled entries
|
- Parse simple `/etc/hosts` with comments and disabled entries
|
||||||
|
|
|
@ -32,14 +32,13 @@ hosts/
|
||||||
### Current State
|
### Current State
|
||||||
- ✅ **Complete uv project**: Python 3.13 with full dependency management
|
- ✅ **Complete uv project**: Python 3.13 with full dependency management
|
||||||
- ✅ **Production application**: Fully functional TUI with advanced features and professional interface
|
- ✅ **Production application**: Fully functional TUI with advanced features and professional interface
|
||||||
- ✅ **Clean code quality**: All ruff linting and formatting checks passing
|
- ✅ **Code quality maintenance required**: 20 linting issues (unused imports/variables) need cleanup
|
||||||
- ✅ **Proper project structure**: Well-organized src/hosts/ package with core and tui modules
|
- ✅ **Proper project structure**: Well-organized src/hosts/ package with core and tui modules
|
||||||
- ✅ **Test coverage restored**: All 149 tests passing after successful test stabilization
|
- ✅ **Comprehensive testing**: 149 tests covering all functionality including new features
|
||||||
- ✅ **Entry point configured**: `hosts` command launches application perfectly
|
- ✅ **Entry point configured**: `hosts` command launches application perfectly
|
||||||
- ✅ **Configuration system**: Complete settings management with JSON persistence
|
- ✅ **Configuration system**: Complete settings management with JSON persistence
|
||||||
- ✅ **Modal interface**: Professional configuration and save confirmation dialogs
|
- ✅ **Modal interface**: Professional configuration dialogs with keyboard bindings
|
||||||
- ✅ **Advanced features**: Sorting, filtering, edit mode, and comprehensive TUI functionality
|
- ✅ **Advanced features**: Sorting, filtering, edit mode, and save confirmation
|
||||||
- ❌ **Known improvements needed**: Status appearance, entry details consistency, sudo handling (per todo.md)
|
|
||||||
|
|
||||||
### Runtime Management
|
### Runtime Management
|
||||||
- ✅ **uv run hosts**: Command executes application instantly
|
- ✅ **uv run hosts**: Command executes application instantly
|
||||||
|
@ -95,9 +94,9 @@ hosts = "hosts.main:main"
|
||||||
|
|
||||||
### Development Workflow
|
### Development Workflow
|
||||||
1. ✅ **uv run hosts**: Execute the application - launches instantly
|
1. ✅ **uv run hosts**: Execute the application - launches instantly
|
||||||
2. ✅ **uv run ruff check**: Lint code - all checks currently passing
|
2. 🔧 **uv run ruff check**: Lint code - 20 issues need fixing
|
||||||
3. ✅ **uv run ruff format**: Auto-format code - consistent style maintained
|
3. ✅ **uv run ruff format**: Auto-format code - consistent style maintained
|
||||||
4. ✅ **uv run pytest**: Run test suite - All 149 tests passing with 100% success rate (test stabilization completed)
|
4. ✅ **uv run pytest**: Run test suite - 149 tests passing with 100% success rate
|
||||||
5. ✅ **uv add**: Add dependencies - seamless dependency management
|
5. ✅ **uv add**: Add dependencies - seamless dependency management
|
||||||
|
|
||||||
### Code Quality Status
|
### Code Quality Status
|
||||||
|
|
|
@ -44,8 +44,8 @@ class HostsManagerApp(App):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.title = "/etc/hosts Manager"
|
self.title = "Hosts Manager"
|
||||||
self.sub_title = "" # Will be set by update_status
|
self.sub_title = "Read-only mode"
|
||||||
|
|
||||||
# Initialize core components
|
# Initialize core components
|
||||||
self.parser = HostsParser()
|
self.parser = HostsParser()
|
||||||
|
@ -75,7 +75,7 @@ class HostsManagerApp(App):
|
||||||
# Right pane - entry details or edit form
|
# Right pane - entry details or edit form
|
||||||
with Vertical(classes="right-pane"):
|
with Vertical(classes="right-pane"):
|
||||||
yield Static("Entry Details", id="details-title")
|
yield Static("Entry Details", id="details-title")
|
||||||
yield DataTable(id="entry-details-table", show_header=False, show_cursor=False, disabled=True)
|
yield Static("Select an entry to view details", id="entry-details")
|
||||||
|
|
||||||
# Edit form (initially hidden)
|
# Edit form (initially hidden)
|
||||||
with Vertical(id="entry-edit-form", classes="hidden"):
|
with Vertical(id="entry-edit-form", classes="hidden"):
|
||||||
|
@ -87,9 +87,6 @@ class HostsManagerApp(App):
|
||||||
yield Input(placeholder="Enter comment (optional)", id="comment-input")
|
yield Input(placeholder="Enter comment (optional)", id="comment-input")
|
||||||
yield Checkbox("Active", id="active-checkbox")
|
yield Checkbox("Active", id="active-checkbox")
|
||||||
|
|
||||||
# Status bar for error/temporary messages (overlay, doesn't affect layout)
|
|
||||||
yield Static("", id="status-bar", classes="status-bar hidden")
|
|
||||||
|
|
||||||
def on_ready(self) -> None:
|
def on_ready(self) -> None:
|
||||||
"""Called when the app is ready."""
|
"""Called when the app is ready."""
|
||||||
self.load_hosts_file()
|
self.load_hosts_file()
|
||||||
|
@ -114,40 +111,30 @@ class HostsManagerApp(App):
|
||||||
self.update_status(f"❌ Error loading hosts file: {e}")
|
self.update_status(f"❌ Error loading hosts file: {e}")
|
||||||
|
|
||||||
def update_status(self, message: str = "") -> None:
|
def update_status(self, message: str = "") -> None:
|
||||||
"""Update the header subtitle and status bar with status information."""
|
"""Update the footer subtitle with status information."""
|
||||||
if message:
|
if message:
|
||||||
# Show temporary message in the status bar
|
# Set temporary status message
|
||||||
try:
|
self.sub_title = message
|
||||||
status_bar = self.query_one("#status-bar", Static)
|
if message.startswith("❌"):
|
||||||
status_bar.update(message)
|
# Auto-clear error message after 5 seconds
|
||||||
status_bar.remove_class("hidden")
|
self.set_timer(5.0, lambda: self.update_status())
|
||||||
|
else:
|
||||||
|
# Auto-clear regular message after 3 seconds
|
||||||
|
self.set_timer(3.0, lambda: self.update_status())
|
||||||
|
else:
|
||||||
|
# Reset to normal status display
|
||||||
|
mode = "Edit mode" if self.edit_mode else "Read-only mode"
|
||||||
|
entry_count = len(self.hosts_file.entries)
|
||||||
|
active_count = len(self.hosts_file.get_active_entries())
|
||||||
|
|
||||||
if message.startswith("❌"):
|
status_text = f"{mode} | {entry_count} entries ({active_count} active)"
|
||||||
# Auto-clear error message after 5 seconds
|
|
||||||
self.set_timer(5.0, lambda: self._clear_status_message())
|
|
||||||
else:
|
|
||||||
# Auto-clear regular message after 3 seconds
|
|
||||||
self.set_timer(3.0, lambda: self._clear_status_message())
|
|
||||||
except:
|
|
||||||
# Fallback if status bar not found (during initialization)
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Always update the header subtitle with current status
|
# Add file info
|
||||||
mode = "Edit mode" if self.edit_mode else "Read-only mode"
|
file_info = self.parser.get_file_info()
|
||||||
entry_count = len(self.hosts_file.entries)
|
if file_info["exists"]:
|
||||||
active_count = len(self.hosts_file.get_active_entries())
|
status_text += f" | {file_info['path']}"
|
||||||
|
|
||||||
# Format: "29 entries (6 active) | Read-only mode"
|
self.sub_title = status_text
|
||||||
self.sub_title = f"{entry_count} entries ({active_count} active) | {mode}"
|
|
||||||
|
|
||||||
def _clear_status_message(self) -> None:
|
|
||||||
"""Clear the temporary status message."""
|
|
||||||
try:
|
|
||||||
status_bar = self.query_one("#status-bar", Static)
|
|
||||||
status_bar.update("")
|
|
||||||
status_bar.add_class("hidden")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Event handlers
|
# Event handlers
|
||||||
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
||||||
|
|
|
@ -5,7 +5,7 @@ This module handles the display and updating of entry details
|
||||||
and edit forms in the right pane.
|
and edit forms in the right pane.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from textual.widgets import Static, Input, Checkbox, DataTable
|
from textual.widgets import Static, Input, Checkbox
|
||||||
|
|
||||||
|
|
||||||
class DetailsHandler:
|
class DetailsHandler:
|
||||||
|
@ -23,30 +23,22 @@ class DetailsHandler:
|
||||||
self.update_details_display()
|
self.update_details_display()
|
||||||
|
|
||||||
def update_details_display(self) -> None:
|
def update_details_display(self) -> None:
|
||||||
"""Update the details display using a DataTable with labeled rows."""
|
"""Update the static details display."""
|
||||||
details_table = self.app.query_one("#entry-details-table", DataTable)
|
details_widget = self.app.query_one("#entry-details", Static)
|
||||||
edit_form = self.app.query_one("#entry-edit-form")
|
edit_form = self.app.query_one("#entry-edit-form")
|
||||||
|
|
||||||
# Show details table, hide edit form
|
# Show details, hide edit form
|
||||||
details_table.remove_class("hidden")
|
details_widget.remove_class("hidden")
|
||||||
edit_form.add_class("hidden")
|
edit_form.add_class("hidden")
|
||||||
|
|
||||||
# Clear existing data
|
|
||||||
details_table.clear()
|
|
||||||
|
|
||||||
if not self.app.hosts_file.entries:
|
if not self.app.hosts_file.entries:
|
||||||
# Show empty message in a single row
|
details_widget.update("No entries loaded")
|
||||||
if not details_table.columns:
|
|
||||||
details_table.add_column("Field", key="field")
|
|
||||||
details_table.add_row("No entries loaded")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get visible entries to check if we need to adjust selection
|
# Get visible entries to check if we need to adjust selection
|
||||||
visible_entries = self.app.table_handler.get_visible_entries()
|
visible_entries = self.app.table_handler.get_visible_entries()
|
||||||
if not visible_entries:
|
if not visible_entries:
|
||||||
if not details_table.columns:
|
details_widget.update("No visible entries")
|
||||||
details_table.add_column("Field", key="field")
|
|
||||||
details_table.add_row("No visible entries")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# If default entries are hidden and selected_entry_index points to a hidden entry,
|
# If default entries are hidden and selected_entry_index points to a hidden entry,
|
||||||
|
@ -73,34 +65,35 @@ class DetailsHandler:
|
||||||
|
|
||||||
entry = self.app.hosts_file.entries[self.app.selected_entry_index]
|
entry = self.app.hosts_file.entries[self.app.selected_entry_index]
|
||||||
|
|
||||||
# Add columns for labeled rows (Field, Value) - only if not already present
|
details_lines = [
|
||||||
if not details_table.columns:
|
f"IP Address: {entry.ip_address}",
|
||||||
details_table.add_column("Field", key="field")
|
f"Hostnames: {', '.join(entry.hostnames)}",
|
||||||
details_table.add_column("Value", key="value")
|
f"Status: {'Active' if entry.is_active else 'Inactive'}",
|
||||||
|
]
|
||||||
# Add rows in the same order as edit form
|
|
||||||
details_table.add_row("IP Address", entry.ip_address, key="ip")
|
|
||||||
details_table.add_row("Hostnames", ", ".join(entry.hostnames), key="hostnames")
|
|
||||||
details_table.add_row("Comment", entry.comment or "", key="comment")
|
|
||||||
details_table.add_row("Active", "Yes" if entry.is_active else "No", key="active")
|
|
||||||
|
|
||||||
# Add DNS name if present (not in edit form but good to show)
|
|
||||||
if entry.dns_name:
|
|
||||||
details_table.add_row("DNS Name", entry.dns_name, key="dns")
|
|
||||||
|
|
||||||
# Add notice for default system entries
|
# Add notice for default system entries
|
||||||
if entry.is_default_entry():
|
if entry.is_default_entry():
|
||||||
details_table.add_row("", "", key="spacer")
|
details_lines.append("")
|
||||||
details_table.add_row("⚠️ WARNING", "SYSTEM DEFAULT ENTRY", key="warning")
|
details_lines.append("⚠️ SYSTEM DEFAULT ENTRY")
|
||||||
details_table.add_row("Note", "This entry cannot be modified", key="note")
|
details_lines.append(
|
||||||
|
"This is a default system entry and cannot be modified."
|
||||||
|
)
|
||||||
|
|
||||||
|
if entry.comment:
|
||||||
|
details_lines.append(f"Comment: {entry.comment}")
|
||||||
|
|
||||||
|
if entry.dns_name:
|
||||||
|
details_lines.append(f"DNS Name: {entry.dns_name}")
|
||||||
|
|
||||||
|
details_widget.update("\n".join(details_lines))
|
||||||
|
|
||||||
def update_edit_form(self) -> None:
|
def update_edit_form(self) -> None:
|
||||||
"""Update the edit form with current entry values."""
|
"""Update the edit form with current entry values."""
|
||||||
details_table = self.app.query_one("#entry-details-table", DataTable)
|
details_widget = self.app.query_one("#entry-details", Static)
|
||||||
edit_form = self.app.query_one("#entry-edit-form")
|
edit_form = self.app.query_one("#entry-edit-form")
|
||||||
|
|
||||||
# Hide details table, show edit form
|
# Hide details, show edit form
|
||||||
details_table.add_class("hidden")
|
details_widget.add_class("hidden")
|
||||||
edit_form.remove_class("hidden")
|
edit_form.remove_class("hidden")
|
||||||
|
|
||||||
if not self.app.hosts_file.entries or self.app.selected_entry_index >= len(
|
if not self.app.hosts_file.entries or self.app.selected_entry_index >= len(
|
||||||
|
|
|
@ -57,21 +57,6 @@ HOSTS_MANAGER_CSS = """
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-bar {
|
|
||||||
height: 1;
|
|
||||||
width: 100%;
|
|
||||||
background: $error;
|
|
||||||
color: $text;
|
|
||||||
content-align: center middle;
|
|
||||||
layer: overlay;
|
|
||||||
dock: top;
|
|
||||||
offset-y: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-bar.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#entry-edit-form {
|
#entry-edit-form {
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
|
@ -90,21 +75,4 @@ HOSTS_MANAGER_CSS = """
|
||||||
#entry-edit-form Checkbox {
|
#entry-edit-form Checkbox {
|
||||||
margin-bottom: 1;
|
margin-bottom: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Entry details table styling */
|
|
||||||
#entry-details-table {
|
|
||||||
background: $background;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#entry-details-table .datatable--even-row {
|
|
||||||
background: $background;
|
|
||||||
}
|
|
||||||
|
|
||||||
#entry-details-table .datatable--odd-row {
|
|
||||||
background: $surface;
|
|
||||||
}
|
|
||||||
|
|
||||||
Header { height: 1; }
|
|
||||||
Header.-tall { height: 1; } /* Fix tall header also to height 1 */
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -22,8 +22,8 @@ class TestHostsManagerApp:
|
||||||
with patch('hosts.tui.app.HostsParser'), patch('hosts.tui.app.Config'):
|
with patch('hosts.tui.app.HostsParser'), patch('hosts.tui.app.Config'):
|
||||||
app = HostsManagerApp()
|
app = HostsManagerApp()
|
||||||
|
|
||||||
assert app.title == "/etc/hosts Manager"
|
assert app.title == "Hosts Manager"
|
||||||
assert app.sub_title == "" # Now set by update_status
|
assert app.sub_title == "Read-only mode"
|
||||||
assert app.edit_mode is False
|
assert app.edit_mode is False
|
||||||
assert app.selected_entry_index == 0
|
assert app.selected_entry_index == 0
|
||||||
assert app.sort_column == ""
|
assert app.sort_column == ""
|
||||||
|
@ -74,7 +74,7 @@ class TestHostsManagerApp:
|
||||||
"""Test handling of missing hosts file."""
|
"""Test handling of missing hosts file."""
|
||||||
mock_parser = Mock(spec=HostsParser)
|
mock_parser = Mock(spec=HostsParser)
|
||||||
mock_config = Mock(spec=Config)
|
mock_config = Mock(spec=Config)
|
||||||
mock_parser.parse.side_effect = FileNotFoundError("Hosts file not found")
|
mock_parser.parse.side_effect = FileNotFoundError("File not found")
|
||||||
|
|
||||||
with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \
|
with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \
|
||||||
patch('hosts.tui.app.Config', return_value=mock_config):
|
patch('hosts.tui.app.Config', return_value=mock_config):
|
||||||
|
@ -85,7 +85,7 @@ class TestHostsManagerApp:
|
||||||
app.load_hosts_file()
|
app.load_hosts_file()
|
||||||
|
|
||||||
# Should handle error gracefully
|
# Should handle error gracefully
|
||||||
app.update_status.assert_called_with("❌ Error loading hosts file: Hosts file not found")
|
app.update_status.assert_called_with("Error: Hosts file not found")
|
||||||
|
|
||||||
def test_load_hosts_file_permission_error(self):
|
def test_load_hosts_file_permission_error(self):
|
||||||
"""Test handling of permission denied error."""
|
"""Test handling of permission denied error."""
|
||||||
|
@ -102,7 +102,7 @@ class TestHostsManagerApp:
|
||||||
app.load_hosts_file()
|
app.load_hosts_file()
|
||||||
|
|
||||||
# Should handle error gracefully
|
# Should handle error gracefully
|
||||||
app.update_status.assert_called_with("❌ Error loading hosts file: Permission denied")
|
app.update_status.assert_called_with("Error: Permission denied")
|
||||||
|
|
||||||
def test_populate_entries_table_logic(self):
|
def test_populate_entries_table_logic(self):
|
||||||
"""Test populating DataTable logic without UI dependencies."""
|
"""Test populating DataTable logic without UI dependencies."""
|
||||||
|
@ -142,26 +142,15 @@ class TestHostsManagerApp:
|
||||||
"""Test updating entry details pane."""
|
"""Test updating entry details pane."""
|
||||||
mock_parser = Mock(spec=HostsParser)
|
mock_parser = Mock(spec=HostsParser)
|
||||||
mock_config = Mock(spec=Config)
|
mock_config = Mock(spec=Config)
|
||||||
mock_config.should_show_default_entries.return_value = True
|
|
||||||
|
|
||||||
with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \
|
with patch('hosts.tui.app.HostsParser', return_value=mock_parser), \
|
||||||
patch('hosts.tui.app.Config', return_value=mock_config):
|
patch('hosts.tui.app.Config', return_value=mock_config):
|
||||||
|
|
||||||
app = HostsManagerApp()
|
app = HostsManagerApp()
|
||||||
|
|
||||||
# Mock the query_one method to return DataTable mock
|
# Mock the query_one method
|
||||||
mock_details_table = Mock()
|
mock_details = Mock()
|
||||||
mock_details_table.columns = [] # Mock empty columns list
|
app.query_one = Mock(return_value=mock_details)
|
||||||
mock_edit_form = Mock()
|
|
||||||
|
|
||||||
def mock_query_one(selector, widget_type=None):
|
|
||||||
if selector == "#entry-details-table":
|
|
||||||
return mock_details_table
|
|
||||||
elif selector == "#entry-edit-form":
|
|
||||||
return mock_edit_form
|
|
||||||
return Mock()
|
|
||||||
|
|
||||||
app.query_one = mock_query_one
|
|
||||||
|
|
||||||
# Add test entry
|
# Add test entry
|
||||||
app.hosts_file = HostsFile()
|
app.hosts_file = HostsFile()
|
||||||
|
@ -175,12 +164,12 @@ class TestHostsManagerApp:
|
||||||
|
|
||||||
app.update_entry_details()
|
app.update_entry_details()
|
||||||
|
|
||||||
# Verify DataTable operations were called
|
# Verify update was called with content containing entry details
|
||||||
mock_details_table.remove_class.assert_called_with("hidden")
|
mock_details.update.assert_called_once()
|
||||||
mock_edit_form.add_class.assert_called_with("hidden")
|
call_args = mock_details.update.call_args[0][0]
|
||||||
mock_details_table.clear.assert_called_once()
|
assert "127.0.0.1" in call_args
|
||||||
mock_details_table.add_column.assert_called()
|
assert "localhost, local" in call_args
|
||||||
mock_details_table.add_row.assert_called()
|
assert "Test comment" in call_args
|
||||||
|
|
||||||
def test_update_entry_details_no_entries(self):
|
def test_update_entry_details_no_entries(self):
|
||||||
"""Test updating entry details with no entries."""
|
"""Test updating entry details with no entries."""
|
||||||
|
@ -192,29 +181,16 @@ class TestHostsManagerApp:
|
||||||
|
|
||||||
app = HostsManagerApp()
|
app = HostsManagerApp()
|
||||||
|
|
||||||
# Mock the query_one method to return DataTable mock
|
# Mock the query_one method
|
||||||
mock_details_table = Mock()
|
mock_details = Mock()
|
||||||
mock_details_table.columns = [] # Mock empty columns list
|
app.query_one = Mock(return_value=mock_details)
|
||||||
mock_edit_form = Mock()
|
|
||||||
|
|
||||||
def mock_query_one(selector, widget_type=None):
|
|
||||||
if selector == "#entry-details-table":
|
|
||||||
return mock_details_table
|
|
||||||
elif selector == "#entry-edit-form":
|
|
||||||
return mock_edit_form
|
|
||||||
return Mock()
|
|
||||||
|
|
||||||
app.query_one = mock_query_one
|
|
||||||
app.hosts_file = HostsFile()
|
app.hosts_file = HostsFile()
|
||||||
|
|
||||||
app.update_entry_details()
|
app.update_entry_details()
|
||||||
|
|
||||||
# Verify DataTable operations were called for empty state
|
# Verify update was called with "No entries loaded"
|
||||||
mock_details_table.remove_class.assert_called_with("hidden")
|
mock_details.update.assert_called_once_with("No entries loaded")
|
||||||
mock_edit_form.add_class.assert_called_with("hidden")
|
|
||||||
mock_details_table.clear.assert_called_once()
|
|
||||||
mock_details_table.add_column.assert_called_with("Field", key="field")
|
|
||||||
mock_details_table.add_row.assert_called_with("No entries loaded")
|
|
||||||
|
|
||||||
def test_update_status_default(self):
|
def test_update_status_default(self):
|
||||||
"""Test status bar update with default information."""
|
"""Test status bar update with default information."""
|
||||||
|
@ -231,6 +207,10 @@ class TestHostsManagerApp:
|
||||||
|
|
||||||
app = HostsManagerApp()
|
app = HostsManagerApp()
|
||||||
|
|
||||||
|
# Mock the query_one method
|
||||||
|
mock_status = Mock()
|
||||||
|
app.query_one = Mock(return_value=mock_status)
|
||||||
|
|
||||||
# Add test entries
|
# Add test entries
|
||||||
app.hosts_file = HostsFile()
|
app.hosts_file = HostsFile()
|
||||||
app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]))
|
app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]))
|
||||||
|
@ -242,10 +222,12 @@ class TestHostsManagerApp:
|
||||||
|
|
||||||
app.update_status()
|
app.update_status()
|
||||||
|
|
||||||
# Verify sub_title was set correctly
|
# Verify status was updated
|
||||||
assert "Read-only mode" in app.sub_title
|
mock_status.update.assert_called_once()
|
||||||
assert "2 entries" in app.sub_title
|
call_args = mock_status.update.call_args[0][0]
|
||||||
assert "1 active" in app.sub_title
|
assert "Read-only mode" in call_args
|
||||||
|
assert "2 entries" in call_args
|
||||||
|
assert "1 active" in call_args
|
||||||
|
|
||||||
def test_update_status_custom_message(self):
|
def test_update_status_custom_message(self):
|
||||||
"""Test status bar update with custom message."""
|
"""Test status bar update with custom message."""
|
||||||
|
@ -257,24 +239,15 @@ class TestHostsManagerApp:
|
||||||
|
|
||||||
app = HostsManagerApp()
|
app = HostsManagerApp()
|
||||||
|
|
||||||
# Mock set_timer and query_one to avoid event loop and UI issues
|
# Mock the query_one method and set_timer to avoid event loop issues
|
||||||
app.set_timer = Mock()
|
mock_status = Mock()
|
||||||
mock_status_bar = Mock()
|
app.query_one = Mock(return_value=mock_status)
|
||||||
app.query_one = Mock(return_value=mock_status_bar)
|
app.set_timer = Mock() # Mock the timer to avoid event loop issues
|
||||||
|
|
||||||
# Add test hosts_file for subtitle generation
|
|
||||||
app.hosts_file = HostsFile()
|
|
||||||
app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]))
|
|
||||||
app.hosts_file.add_entry(HostEntry(ip_address="192.168.1.1", hostnames=["router"], is_active=False))
|
|
||||||
|
|
||||||
app.update_status("Custom status message")
|
app.update_status("Custom status message")
|
||||||
|
|
||||||
# Verify status bar was updated with custom message
|
# Verify status was updated with custom message
|
||||||
mock_status_bar.update.assert_called_with("Custom status message")
|
mock_status.update.assert_called_once_with("Custom status message")
|
||||||
mock_status_bar.remove_class.assert_called_with("hidden")
|
|
||||||
# Verify subtitle shows current status (not the custom message)
|
|
||||||
assert "2 entries" in app.sub_title
|
|
||||||
assert "Read-only mode" in app.sub_title
|
|
||||||
# Verify timer was set for auto-clearing
|
# Verify timer was set for auto-clearing
|
||||||
app.set_timer.assert_called_once()
|
app.set_timer.assert_called_once()
|
||||||
|
|
||||||
|
@ -347,21 +320,19 @@ class TestHostsManagerApp:
|
||||||
app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]))
|
app.hosts_file.add_entry(HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]))
|
||||||
app.hosts_file.add_entry(HostEntry(ip_address="10.0.0.1", hostnames=["test"]))
|
app.hosts_file.add_entry(HostEntry(ip_address="10.0.0.1", hostnames=["test"]))
|
||||||
|
|
||||||
# Mock the table_handler methods to avoid UI queries
|
app.populate_entries_table = Mock()
|
||||||
app.table_handler.populate_entries_table = Mock()
|
|
||||||
app.table_handler.restore_cursor_position = Mock()
|
|
||||||
app.update_status = Mock()
|
app.update_status = Mock()
|
||||||
|
|
||||||
app.action_sort_by_ip()
|
app.action_sort_by_ip()
|
||||||
|
|
||||||
# Check that entries are sorted by IP address
|
# Check that entries are sorted with default entries on top
|
||||||
assert app.hosts_file.entries[0].ip_address == "10.0.0.1" # Sorted by IP
|
assert app.hosts_file.entries[0].ip_address == "127.0.0.1" # Default entry first
|
||||||
assert app.hosts_file.entries[1].ip_address == "127.0.0.1"
|
assert app.hosts_file.entries[1].ip_address == "10.0.0.1" # Then sorted non-defaults
|
||||||
assert app.hosts_file.entries[2].ip_address == "192.168.1.1"
|
assert app.hosts_file.entries[2].ip_address == "192.168.1.1"
|
||||||
|
|
||||||
assert app.sort_column == "ip"
|
assert app.sort_column == "ip"
|
||||||
assert app.sort_ascending is True
|
assert app.sort_ascending is True
|
||||||
app.table_handler.populate_entries_table.assert_called_once()
|
app.populate_entries_table.assert_called_once()
|
||||||
|
|
||||||
def test_action_sort_by_hostname_ascending(self):
|
def test_action_sort_by_hostname_ascending(self):
|
||||||
"""Test sorting by hostname in ascending order."""
|
"""Test sorting by hostname in ascending order."""
|
||||||
|
@ -379,9 +350,7 @@ class TestHostsManagerApp:
|
||||||
app.hosts_file.add_entry(HostEntry(ip_address="192.168.1.1", hostnames=["alpha"]))
|
app.hosts_file.add_entry(HostEntry(ip_address="192.168.1.1", hostnames=["alpha"]))
|
||||||
app.hosts_file.add_entry(HostEntry(ip_address="10.0.0.1", hostnames=["beta"]))
|
app.hosts_file.add_entry(HostEntry(ip_address="10.0.0.1", hostnames=["beta"]))
|
||||||
|
|
||||||
# Mock the table_handler methods to avoid UI queries
|
app.populate_entries_table = Mock()
|
||||||
app.table_handler.populate_entries_table = Mock()
|
|
||||||
app.table_handler.restore_cursor_position = Mock()
|
|
||||||
app.update_status = Mock()
|
app.update_status = Mock()
|
||||||
|
|
||||||
app.action_sort_by_hostname()
|
app.action_sort_by_hostname()
|
||||||
|
@ -393,7 +362,7 @@ class TestHostsManagerApp:
|
||||||
|
|
||||||
assert app.sort_column == "hostname"
|
assert app.sort_column == "hostname"
|
||||||
assert app.sort_ascending is True
|
assert app.sort_ascending is True
|
||||||
app.table_handler.populate_entries_table.assert_called_once()
|
app.populate_entries_table.assert_called_once()
|
||||||
|
|
||||||
def test_data_table_row_highlighted_event(self):
|
def test_data_table_row_highlighted_event(self):
|
||||||
"""Test DataTable row highlighting event handling."""
|
"""Test DataTable row highlighting event handling."""
|
||||||
|
@ -404,10 +373,10 @@ class TestHostsManagerApp:
|
||||||
patch('hosts.tui.app.Config', return_value=mock_config):
|
patch('hosts.tui.app.Config', return_value=mock_config):
|
||||||
|
|
||||||
app = HostsManagerApp()
|
app = HostsManagerApp()
|
||||||
|
app.update_entry_details = Mock()
|
||||||
|
|
||||||
# Mock the details_handler and table_handler methods
|
# Mock the display_index_to_actual_index method to return the same index
|
||||||
app.details_handler.update_entry_details = Mock()
|
app.display_index_to_actual_index = Mock(return_value=2)
|
||||||
app.table_handler.display_index_to_actual_index = Mock(return_value=2)
|
|
||||||
|
|
||||||
# Create mock event with required parameters
|
# Create mock event with required parameters
|
||||||
mock_table = Mock()
|
mock_table = Mock()
|
||||||
|
@ -420,8 +389,8 @@ class TestHostsManagerApp:
|
||||||
|
|
||||||
# Should update selected index and details
|
# Should update selected index and details
|
||||||
assert app.selected_entry_index == 2
|
assert app.selected_entry_index == 2
|
||||||
app.details_handler.update_entry_details.assert_called_once()
|
app.update_entry_details.assert_called_once()
|
||||||
app.table_handler.display_index_to_actual_index.assert_called_once_with(2)
|
app.display_index_to_actual_index.assert_called_once_with(2)
|
||||||
|
|
||||||
def test_data_table_header_selected_ip_column(self):
|
def test_data_table_header_selected_ip_column(self):
|
||||||
"""Test DataTable header selection for IP column."""
|
"""Test DataTable header selection for IP column."""
|
||||||
|
|
|
@ -279,9 +279,7 @@ class TestSaveConfirmationIntegration:
|
||||||
"""Test exit_edit_entry_mode cleans up properly."""
|
"""Test exit_edit_entry_mode cleans up properly."""
|
||||||
app.entry_edit_mode = True
|
app.entry_edit_mode = True
|
||||||
app.original_entry_values = {"test": "data"}
|
app.original_entry_values = {"test": "data"}
|
||||||
|
app.update_entry_details = Mock()
|
||||||
# Mock the details_handler and query_one methods
|
|
||||||
app.details_handler.update_entry_details = Mock()
|
|
||||||
app.query_one = Mock()
|
app.query_one = Mock()
|
||||||
app.update_status = Mock()
|
app.update_status = Mock()
|
||||||
|
|
||||||
|
@ -292,6 +290,6 @@ class TestSaveConfirmationIntegration:
|
||||||
|
|
||||||
assert not app.entry_edit_mode
|
assert not app.entry_edit_mode
|
||||||
assert app.original_entry_values is None
|
assert app.original_entry_values is None
|
||||||
app.details_handler.update_entry_details.assert_called_once()
|
app.update_entry_details.assert_called_once()
|
||||||
mock_table.focus.assert_called_once()
|
mock_table.focus.assert_called_once()
|
||||||
app.update_status.assert_called_once_with("Exited entry edit mode")
|
app.update_status.assert_called_once_with("Exited entry edit mode")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue