diff options
| author | Warry <FunWarry@users.noreply.github.com> | 2025-11-30 15:37:32 +0100 |
|---|---|---|
| committer | Warry <FunWarry@users.noreply.github.com> | 2025-11-30 15:37:32 +0100 |
| commit | 121dbb788d051c76375261cd18fa8dca0f23b935 (patch) | |
| tree | cab3f2df3aa9e1437fdd0b5c60ab22780f8d717d /tests/test_settings_dialog_structure.py | |
| parent | 28a6fe5a7dbadc9532de228a539c47ec49ca4aa2 (diff) | |
| download | tidal-dl-ng-121dbb788d051c76375261cd18fa8dca0f23b935.tar.gz | |
feat: refactor settings UI with category navigation and add Delimiters page
Closes #640
## Changes
### UI Refactoring
- Replace monolithic settings dialog with category-based navigation
- Add QListWidget (lw_categories) for category selection on the left
- Add QStackedWidget (sw_categories) for content pages on the right
- Organize settings into 5 distinct pages:
- Flags (14 checkboxes)
- Quality (3 combo boxes)
- Numbers (2 spin boxes)
- Paths & Formats (7 line edits + 2 browse buttons)
- Delimiters (4 line edits) — NEW
### New Feature: Delimiters Category
- Add customizable separators for artists in metadata and filenames:
- metadata_delimiter_artist
- metadata_delimiter_album_artist
- filename_delimiter_artist
- filename_delimiter_album_artist
- Auto-integration with DialogPreferences via parameters_line_edit
- Line edits with max width 100px for compact display
### Styling
- Apply dark theme to settings dialog:
- Category list: #2b2b2b background, #3d5a80 selection, #3a3a3a hover
- Settings area: #333333 background, styled GroupBoxes (#3a3a3a)
- Light text (#e0e0e0) for better contrast
- Add spacing property (6px) to category list for better readability
- Style QListWidget items with padding (4px 3px) and margin (2px 0px)
### Testing
- Add comprehensive test suite (61 tests total):
- test_settings_ui.py: UI and basic integration (14 tests)
- test_settings_dialog_structure.py: detailed structure validation (20 tests)
- test_dialog_preferences_integration.py: DialogPreferences integration (14 tests)
- test_delimiters_category.py: new Delimiters page validation (13 tests)
- Fix ruff issues (F841, S108) in test files
- All tests passing with 100% success rate
### Documentation
- Add docs/refactor_settings_ui.md: comprehensive refactor overview
- Add docs/delimiters_category.md: Delimiters feature documentation
- Add docs/ui_styles.md: dark theme style guide
- Add docs/testing_summary.md: testing overview and commands
### Code Quality
- Apply ruff/black/pyupgrade via pre-commit hooks
- Fix F841 (unused variable) in test_dialog_preferences_integration.py
- Fix S108 (insecure temp path) using Path.home() in test_settings_ui.py
- End-of-file fixes applied automatically
## Files Modified
- tidal_dl_ng/ui/dialog_settings.ui (complete restructure)
- tidal_dl_ng/ui/dialog_settings.py (auto-generated from .ui)
- tidal_dl_ng/dialog.py (add 4 delimiter params + Delimiters category)
- tests/* (4 new test files with 61 tests)
- docs/* (4 new documentation files)
## Impact
- Improved UX: intuitive category navigation, better visual organization
- Extensibility: easy to add new settings pages/categories
- Quality: comprehensive test coverage ensures robustness
- User flexibility: customizable separators for metadata and filenames
## Technical Notes
- QStackedWidget pages indexed 0-4 matching category list order
- Navigation via QListWidget::currentRowChanged signal
- Automatic save/load through existing DialogPreferences methods
- No breaking changes to existing settings or configuration
## Testing Commands
```powershell
poetry run pre-commit run -a
poetry run pytest -q
Diffstat (limited to 'tests/test_settings_dialog_structure.py')
| -rw-r--r-- | tests/test_settings_dialog_structure.py | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/tests/test_settings_dialog_structure.py b/tests/test_settings_dialog_structure.py new file mode 100644 index 0000000..726776b --- /dev/null +++ b/tests/test_settings_dialog_structure.py @@ -0,0 +1,277 @@ +"""Tests for the settings dialog category structure and organization.""" + +import pytest +from PySide6 import QtCore, QtWidgets + +from tidal_dl_ng.ui.dialog_settings import Ui_DialogSettings + + +@pytest.fixture +def qapp(): + """Create QApplication instance for tests.""" + app = QtWidgets.QApplication.instance() + if app is None: + app = QtWidgets.QApplication([]) + yield app + + +class TestSettingsDialogStructure: + """Test the structural integrity of the settings dialog.""" + + def test_dialog_has_proper_layout_hierarchy(self, qapp): + """Test that the dialog has the correct layout hierarchy.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + # Check main layout + main_layout = dialog.layout() + assert main_layout is not None + assert isinstance(main_layout, QtWidgets.QVBoxLayout) + + def test_categories_and_pages_match(self, qapp): + """Test that the number of pages matches the expected categories.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + # We expect 4 categories/pages + expected_categories = ["Flags", "Quality", "Numbers", "Paths & Formats"] + assert ui.sw_categories.count() == len(expected_categories) + + def test_all_pages_have_layouts(self, qapp): + """Test that all pages in the stacked widget have proper layouts.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + for i in range(ui.sw_categories.count()): + page = ui.sw_categories.widget(i) + assert page is not None + assert page.layout() is not None + assert isinstance(page.layout(), QtWidgets.QVBoxLayout) + + def test_all_pages_have_group_boxes(self, qapp): + """Test that all pages contain at least one group box.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + for i in range(ui.sw_categories.count()): + page = ui.sw_categories.widget(i) + group_boxes = page.findChildren(QtWidgets.QGroupBox) + assert len(group_boxes) > 0, f"Page {i} should have at least one group box" + + +class TestSettingsFlagsPage: + """Test the Flags page specifically.""" + + def test_flags_page_is_first(self, qapp): + """Test that the Flags page is at index 0.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + assert ui.page_flags is not None + assert ui.sw_categories.widget(0) == ui.page_flags + + def test_flags_page_has_correct_number_of_checkboxes(self, qapp): + """Test that the Flags page has the expected number of checkboxes.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + # Count checkboxes in the flags page + checkboxes = ui.page_flags.findChildren(QtWidgets.QCheckBox) + assert len(checkboxes) == 14, "Flags page should have 14 checkboxes" + + def test_all_flags_checkboxes_accessible_via_ui(self, qapp): + """Test that all flag checkboxes are accessible through the ui object.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + checkbox_names = [ + "cb_video_download", + "cb_video_convert_mp4", + "cb_lyrics_embed", + "cb_lyrics_file", + "cb_download_delay", + "cb_extract_flac", + "cb_metadata_cover_embed", + "cb_cover_album_file", + "cb_skip_existing", + "cb_symlink_to_track", + "cb_playlist_create", + "cb_mark_explicit", + "cb_use_primary_album_artist", + "cb_download_dolby_atmos", + ] + + for name in checkbox_names: + checkbox = getattr(ui, name, None) + assert checkbox is not None, f"Checkbox {name} not found" + assert isinstance(checkbox, QtWidgets.QCheckBox) + + +class TestSettingsQualityPage: + """Test the Quality page specifically.""" + + def test_quality_page_is_second(self, qapp): + """Test that the Quality page is at index 1.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + assert ui.page_quality is not None + assert ui.sw_categories.widget(1) == ui.page_quality + + def test_quality_page_has_combo_boxes(self, qapp): + """Test that the Quality page has combo boxes.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + combos = ui.page_quality.findChildren(QtWidgets.QComboBox) + assert len(combos) == 3, "Quality page should have 3 combo boxes" + + def test_quality_combo_boxes_have_labels(self, qapp): + """Test that each combo box has associated labels.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + combo_label_pairs = [ + ("c_quality_audio", "l_quality_audio", "l_icon_quality_audio"), + ("c_quality_video", "l_quality_video", "l_icon_quality_video"), + ("c_metadata_cover_dimension", "l_metadata_cover_dimension", "l_icon_metadata_cover_dimension"), + ] + + for combo_name, label_name, icon_label_name in combo_label_pairs: + combo = getattr(ui, combo_name, None) + label = getattr(ui, label_name, None) + icon_label = getattr(ui, icon_label_name, None) + + assert combo is not None, f"ComboBox {combo_name} not found" + assert label is not None, f"Label {label_name} not found" + assert icon_label is not None, f"Icon label {icon_label_name} not found" + + +class TestSettingsNumbersPage: + """Test the Numbers page specifically.""" + + def test_numbers_page_is_third(self, qapp): + """Test that the Numbers page is at index 2.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + assert ui.page_numbers is not None + assert ui.sw_categories.widget(2) == ui.page_numbers + + def test_numbers_page_has_spin_boxes(self, qapp): + """Test that the Numbers page has spin boxes.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + spinboxes = ui.page_numbers.findChildren(QtWidgets.QSpinBox) + assert len(spinboxes) == 2, "Numbers page should have 2 spin boxes" + + def test_spin_boxes_have_valid_ranges(self, qapp): + """Test that spin boxes have appropriate min/max values.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + # Test album track num padding + assert ui.sb_album_track_num_pad_min.maximum() == 4 + assert ui.sb_album_track_num_pad_min.minimum() >= 0 + + # Test concurrent downloads + assert ui.sb_downloads_concurrent_max.minimum() == 1 + assert ui.sb_downloads_concurrent_max.maximum() == 5 + + +class TestSettingsPathsPage: + """Test the Paths & Formats page specifically.""" + + def test_paths_page_is_fourth(self, qapp): + """Test that the Paths page is at index 3.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + assert ui.page_paths is not None + assert ui.sw_categories.widget(3) == ui.page_paths + + def test_paths_page_has_line_edits(self, qapp): + """Test that the Paths page has line edits.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + line_edits = ui.page_paths.findChildren(QtWidgets.QLineEdit) + assert len(line_edits) == 7, "Paths page should have 7 line edits" + + def test_paths_page_has_browse_buttons(self, qapp): + """Test that the Paths page has browse buttons.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + buttons = ui.page_paths.findChildren(QtWidgets.QPushButton) + # Should have at least 2 browse buttons (... buttons) + assert len(buttons) >= 2, "Paths page should have at least 2 browse buttons" + + def test_path_line_edits_accessible(self, qapp): + """Test that all path-related line edits are accessible.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + line_edit_names = [ + "le_download_base_path", + "le_format_track", + "le_format_video", + "le_format_album", + "le_format_playlist", + "le_format_mix", + "le_path_binary_ffmpeg", + ] + + for name in line_edit_names: + line_edit = getattr(ui, name, None) + assert line_edit is not None, f"LineEdit {name} not found" + assert isinstance(line_edit, QtWidgets.QLineEdit) + + +class TestSettingsDialogButtons: + """Test the dialog button box.""" + + def test_dialog_has_button_box(self, qapp): + """Test that the dialog has a button box.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + assert ui.bb_dialog is not None + assert isinstance(ui.bb_dialog, QtWidgets.QDialogButtonBox) + + def test_button_box_has_ok_and_cancel(self, qapp): + """Test that the button box has OK and Cancel buttons.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + buttons = ui.bb_dialog.standardButtons() + assert QtWidgets.QDialogButtonBox.StandardButton.Ok in buttons + assert QtWidgets.QDialogButtonBox.StandardButton.Cancel in buttons + + def test_button_box_orientation(self, qapp): + """Test that the button box has horizontal orientation.""" + dialog = QtWidgets.QDialog() + ui = Ui_DialogSettings() + ui.setupUi(dialog) + + assert ui.bb_dialog.orientation() == QtCore.Qt.Orientation.Horizontal |
