Files
tidal-dl/tests/test_delimiters_category.py
T
Warry 121dbb788d 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
2025-11-30 15:37:32 +01:00

258 lines
9.1 KiB
Python

"""Tests for the Delimiters category in settings dialog."""
from unittest.mock import Mock, patch
import pytest
from PySide6 import QtWidgets
from tidal_dl_ng.dialog import DialogPreferences
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 TestDelimitersPage:
"""Test the Delimiters page specifically."""
def test_delimiters_page_exists(self, qapp):
"""Test that the Delimiters page exists."""
dialog = QtWidgets.QDialog()
ui = Ui_DialogSettings()
ui.setupUi(dialog)
assert hasattr(ui, "page_delimiters")
assert ui.page_delimiters is not None
def test_delimiters_page_is_fifth(self, qapp):
"""Test that the Delimiters page is at index 4."""
dialog = QtWidgets.QDialog()
ui = Ui_DialogSettings()
ui.setupUi(dialog)
assert ui.sw_categories.widget(4) == ui.page_delimiters
def test_delimiters_groupbox_exists(self, qapp):
"""Test that the Delimiters GroupBox exists."""
dialog = QtWidgets.QDialog()
ui = Ui_DialogSettings()
ui.setupUi(dialog)
assert hasattr(ui, "gb_delimiters")
assert ui.gb_delimiters is not None
assert isinstance(ui.gb_delimiters, QtWidgets.QGroupBox)
def test_delimiters_page_has_four_line_edits(self, qapp):
"""Test that the Delimiters page has 4 line edits."""
dialog = QtWidgets.QDialog()
ui = Ui_DialogSettings()
ui.setupUi(dialog)
line_edits = ui.page_delimiters.findChildren(QtWidgets.QLineEdit)
assert len(line_edits) == 4, "Delimiters page should have 4 line edits"
def test_all_delimiter_line_edits_accessible(self, qapp):
"""Test that all delimiter line edits are accessible."""
dialog = QtWidgets.QDialog()
ui = Ui_DialogSettings()
ui.setupUi(dialog)
line_edit_names = [
"le_metadata_delimiter_artist",
"le_metadata_delimiter_album_artist",
"le_filename_delimiter_artist",
"le_filename_delimiter_album_artist",
]
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)
def test_delimiter_line_edits_have_max_width(self, qapp):
"""Test that delimiter line edits have a maximum width set."""
dialog = QtWidgets.QDialog()
ui = Ui_DialogSettings()
ui.setupUi(dialog)
line_edit_names = [
"le_metadata_delimiter_artist",
"le_metadata_delimiter_album_artist",
"le_filename_delimiter_artist",
"le_filename_delimiter_album_artist",
]
for name in line_edit_names:
line_edit = getattr(ui, name)
# Should have max width of 100px
assert line_edit.maximumWidth() == 100, f"{name} should have max width of 100px"
def test_delimiter_labels_exist(self, qapp):
"""Test that labels and icon labels exist for each delimiter."""
dialog = QtWidgets.QDialog()
ui = Ui_DialogSettings()
ui.setupUi(dialog)
label_names = [
("l_metadata_delimiter_artist", "l_icon_metadata_delimiter_artist"),
("l_metadata_delimiter_album_artist", "l_icon_metadata_delimiter_album_artist"),
("l_filename_delimiter_artist", "l_icon_filename_delimiter_artist"),
("l_filename_delimiter_album_artist", "l_icon_filename_delimiter_album_artist"),
]
for label_name, icon_label_name in label_names:
label = getattr(ui, label_name, None)
icon_label = getattr(ui, icon_label_name, None)
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"
assert isinstance(label, QtWidgets.QLabel)
assert isinstance(icon_label, QtWidgets.QLabel)
class TestDelimitersIntegration:
"""Test Delimiters integration with DialogPreferences."""
@pytest.fixture
def mock_settings(self):
"""Create a mock Settings object with delimiter attributes."""
settings = Mock()
settings.data = Mock()
# Boolean flags
for attr in [
"lyrics_embed",
"lyrics_file",
"use_primary_album_artist",
"video_download",
"download_dolby_atmos",
"download_delay",
"video_convert_mp4",
"extract_flac",
"metadata_cover_embed",
"mark_explicit",
"cover_album_file",
"skip_existing",
"symlink_to_track",
"playlist_create",
]:
setattr(settings.data, attr, False)
# Paths
for attr in [
"download_base_path",
"format_album",
"format_playlist",
"format_mix",
"format_track",
"format_video",
"path_binary_ffmpeg",
]:
setattr(settings.data, attr, f"/test/{attr}")
# Delimiters
settings.data.metadata_delimiter_artist = ", "
settings.data.metadata_delimiter_album_artist = ", "
settings.data.filename_delimiter_artist = ", "
settings.data.filename_delimiter_album_artist = ", "
# Numbers
settings.data.album_track_num_pad_min = 2
settings.data.downloads_concurrent_max = 3
# Enums
settings.data.quality_audio = Mock()
settings.data.quality_video = Mock()
settings.data.metadata_cover_dimension = Mock()
return settings
@patch.object(DialogPreferences, "exec")
def test_delimiters_category_added_to_list(self, mock_exec, mock_settings, qapp):
"""Test that Delimiters category is added to the list."""
from PySide6.QtCore import Signal as QtSignal
settings_save = QtSignal()
with patch.object(DialogPreferences, "gui_populate"):
dialog = DialogPreferences(mock_settings, settings_save)
# Should have 5 categories now
assert dialog.ui.lw_categories.count() == 5
assert dialog.ui.lw_categories.item(4).text() == "Delimiters"
@patch.object(DialogPreferences, "exec")
def test_delimiter_line_edits_in_parameters(self, mock_exec, mock_settings, qapp):
"""Test that delimiter line edits are in parameters_line_edit."""
from PySide6.QtCore import Signal as QtSignal
settings_save = QtSignal()
with patch.object(DialogPreferences, "gui_populate"):
dialog = DialogPreferences(mock_settings, settings_save)
# Check that all 4 delimiters are in the list
assert "metadata_delimiter_artist" in dialog.parameters_line_edit
assert "metadata_delimiter_album_artist" in dialog.parameters_line_edit
assert "filename_delimiter_artist" in dialog.parameters_line_edit
assert "filename_delimiter_album_artist" in dialog.parameters_line_edit
@patch.object(DialogPreferences, "exec")
def test_navigate_to_delimiters_page(self, mock_exec, mock_settings, qapp):
"""Test navigation to Delimiters page."""
from PySide6.QtCore import Signal as QtSignal
settings_save = QtSignal()
with patch.object(DialogPreferences, "gui_populate"):
dialog = DialogPreferences(mock_settings, settings_save)
# Navigate to Delimiters (index 4)
dialog.ui.lw_categories.setCurrentRow(4)
assert dialog.ui.sw_categories.currentIndex() == 4
assert dialog.ui.sw_categories.currentWidget() == dialog.ui.page_delimiters
class TestDelimitersStructure:
"""Test the structural integrity of the Delimiters page."""
def test_stacked_widget_has_five_pages(self, qapp):
"""Test that the stacked widget now contains 5 pages."""
dialog = QtWidgets.QDialog()
ui = Ui_DialogSettings()
ui.setupUi(dialog)
assert ui.sw_categories.count() == 5, "Should have 5 pages including Delimiters"
def test_delimiters_page_has_layout(self, qapp):
"""Test that Delimiters page has a proper layout."""
dialog = QtWidgets.QDialog()
ui = Ui_DialogSettings()
ui.setupUi(dialog)
assert ui.page_delimiters.layout() is not None
assert isinstance(ui.page_delimiters.layout(), QtWidgets.QVBoxLayout)
def test_delimiters_page_has_spacer(self, qapp):
"""Test that Delimiters page has a vertical spacer."""
dialog = QtWidgets.QDialog()
ui = Ui_DialogSettings()
ui.setupUi(dialog)
# Check that there's a spacer in the layout
layout = ui.page_delimiters.layout()
spacer_found = False
for i in range(layout.count()):
item = layout.itemAt(i)
if item.spacerItem():
spacer_found = True
break
assert spacer_found, "Delimiters page should have a vertical spacer"