summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_delimiters_category.py257
-rw-r--r--tests/test_dialog_preferences_integration.py309
-rw-r--r--tests/test_settings_dialog_structure.py277
-rw-r--r--tests/test_settings_ui.py387
4 files changed, 1230 insertions, 0 deletions
diff --git a/tests/test_delimiters_category.py b/tests/test_delimiters_category.py
new file mode 100644
index 0000000..df5e950
--- /dev/null
+++ b/tests/test_delimiters_category.py
@@ -0,0 +1,257 @@
+"""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"
diff --git a/tests/test_dialog_preferences_integration.py b/tests/test_dialog_preferences_integration.py
new file mode 100644
index 0000000..b7c21c2
--- /dev/null
+++ b/tests/test_dialog_preferences_integration.py
@@ -0,0 +1,309 @@
+"""Integration tests for DialogPreferences with the refactored UI."""
+
+from unittest.mock import Mock, patch
+
+import pytest
+from PySide6 import QtCore, QtWidgets
+
+from tidal_dl_ng.config import Settings
+from tidal_dl_ng.dialog import DialogPreferences
+
+
+@pytest.fixture
+def qapp():
+ """Create QApplication instance for tests."""
+ app = QtWidgets.QApplication.instance()
+ if app is None:
+ app = QtWidgets.QApplication([])
+ yield app
+
+
+@pytest.fixture
+def mock_settings():
+ """Create a mock Settings object with all required attributes."""
+ settings = Mock(spec=Settings)
+ settings.data = Mock()
+
+ # Boolean flags
+ bool_attrs = [
+ "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",
+ ]
+ for attr in bool_attrs:
+ setattr(settings.data, attr, False)
+
+ # String paths
+ path_attrs = [
+ "download_base_path",
+ "format_album",
+ "format_playlist",
+ "format_mix",
+ "format_track",
+ "format_video",
+ "path_binary_ffmpeg",
+ ]
+ for attr in path_attrs:
+ setattr(settings.data, attr, f"/test/{attr}")
+
+ # Numeric values
+ settings.data.album_track_num_pad_min = 2
+ settings.data.downloads_concurrent_max = 3
+
+ # Enum values
+ settings.data.quality_audio = Mock()
+ settings.data.quality_video = Mock()
+ settings.data.metadata_cover_dimension = Mock()
+
+ return settings
+
+
+class TestDialogPreferencesInitialization:
+ """Test DialogPreferences initialization with refactored UI."""
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_dialog_initializes_with_ui(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that DialogPreferences initializes with the new UI."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ assert hasattr(dialog, "ui")
+ assert dialog.ui is not None
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_categories_initialized_correctly(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that categories are initialized correctly."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Check categories count
+ assert dialog.ui.lw_categories.count() == 4
+
+ # Check categories text
+ categories = [dialog.ui.lw_categories.item(i).text() for i in range(4)]
+ assert categories == ["Flags", "Quality", "Numbers", "Paths & Formats"]
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_first_category_selected_by_default(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that the first category is selected on initialization."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ assert dialog.ui.lw_categories.currentRow() == 0
+ assert dialog.ui.sw_categories.currentIndex() == 0
+
+
+class TestDialogPreferencesCategoryNavigation:
+ """Test category navigation functionality."""
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_category_change_updates_page(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that changing category updates the displayed page."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Test each category
+ for i in range(4):
+ dialog.ui.lw_categories.setCurrentRow(i)
+ assert dialog.ui.sw_categories.currentIndex() == i
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_on_category_changed_signal_connected(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that the category change signal is properly connected."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Verify the connection by changing category programmatically
+ initial_page = dialog.ui.sw_categories.currentIndex()
+ dialog.ui.lw_categories.setCurrentRow(2)
+ new_page = dialog.ui.sw_categories.currentIndex()
+
+ assert initial_page != new_page
+ assert new_page == 2
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_navigate_through_all_categories_sequentially(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test sequential navigation through all categories."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Navigate forward
+ for i in range(4):
+ dialog.ui.lw_categories.setCurrentRow(i)
+ assert dialog.ui.sw_categories.currentIndex() == i
+ assert dialog.ui.lw_categories.currentRow() == i
+
+ # Navigate backward
+ for i in range(3, -1, -1):
+ dialog.ui.lw_categories.setCurrentRow(i)
+ assert dialog.ui.sw_categories.currentIndex() == i
+ assert dialog.ui.lw_categories.currentRow() == i
+
+
+class TestDialogPreferencesWidgetAccess:
+ """Test access to widgets through DialogPreferences."""
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_access_flags_checkboxes(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that flag checkboxes are accessible."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ checkbox_names = ["cb_video_download", "cb_lyrics_embed", "cb_skip_existing"]
+
+ for name in checkbox_names:
+ checkbox = getattr(dialog.ui, name, None)
+ assert checkbox is not None
+ assert isinstance(checkbox, QtWidgets.QCheckBox)
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_access_quality_combos(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that quality combo boxes are accessible."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ combo_names = ["c_quality_audio", "c_quality_video", "c_metadata_cover_dimension"]
+
+ for name in combo_names:
+ combo = getattr(dialog.ui, name, None)
+ assert combo is not None
+ assert isinstance(combo, QtWidgets.QComboBox)
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_access_numbers_spinboxes(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that number spinboxes are accessible."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ spinbox_names = ["sb_album_track_num_pad_min", "sb_downloads_concurrent_max"]
+
+ for name in spinbox_names:
+ spinbox = getattr(dialog.ui, name, None)
+ assert spinbox is not None
+ assert isinstance(spinbox, QtWidgets.QSpinBox)
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_access_path_line_edits(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that path line edits are accessible."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ line_edit_names = ["le_download_base_path", "le_format_track", "le_path_binary_ffmpeg"]
+
+ for name in line_edit_names:
+ line_edit = getattr(dialog.ui, name, None)
+ assert line_edit is not None
+ assert isinstance(line_edit, QtWidgets.QLineEdit)
+
+
+class TestDialogPreferencesPageVisibility:
+ """Test that pages are correctly shown/hidden based on category selection."""
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_only_current_page_visible(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that only the current page is visible."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ for i in range(4):
+ dialog.ui.lw_categories.setCurrentRow(i)
+ current_widget = dialog.ui.sw_categories.currentWidget()
+
+ # Check that the current widget is the expected page
+ if i == 0:
+ assert current_widget == dialog.ui.page_flags
+ elif i == 1:
+ assert current_widget == dialog.ui.page_quality
+ elif i == 2:
+ assert current_widget == dialog.ui.page_numbers
+ elif i == 3:
+ assert current_widget == dialog.ui.page_paths
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_page_content_accessible_when_selected(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that page content is accessible when the page is selected."""
+ settings_save = QtCore.Signal()
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Select Flags page
+ dialog.ui.lw_categories.setCurrentRow(0)
+ assert dialog.ui.gb_flags.isVisibleTo(dialog.ui.page_flags)
+
+ # Select Quality page
+ dialog.ui.lw_categories.setCurrentRow(1)
+ assert dialog.ui.gb_choices.isVisibleTo(dialog.ui.page_quality)
+
+ # Select Numbers page
+ dialog.ui.lw_categories.setCurrentRow(2)
+ assert dialog.ui.gb_numbers.isVisibleTo(dialog.ui.page_numbers)
+
+ # Select Paths page
+ dialog.ui.lw_categories.setCurrentRow(3)
+ assert dialog.ui.gb_path.isVisibleTo(dialog.ui.page_paths)
+
+
+class TestDialogPreferencesMethodsStillWork:
+ """Test that existing DialogPreferences methods still work with new UI."""
+
+ @patch.object(DialogPreferences, "exec")
+ @patch.object(DialogPreferences, "gui_populate")
+ def test_init_methods_called(self, mock_populate, mock_exec, mock_settings, qapp):
+ """Test that initialization methods are called."""
+ settings_save = QtCore.Signal()
+
+ with (
+ patch.object(DialogPreferences, "_init_checkboxes") as mock_init_cb,
+ patch.object(DialogPreferences, "_init_comboboxes") as mock_init_combo,
+ patch.object(DialogPreferences, "_init_line_edit") as mock_init_le,
+ patch.object(DialogPreferences, "_init_spin_box") as mock_init_sb,
+ patch.object(DialogPreferences, "_init_categories") as mock_init_cat,
+ ):
+
+ # Instantiate to trigger init methods; no need to keep a variable
+ DialogPreferences(mock_settings, settings_save)
+
+ mock_init_cb.assert_called_once()
+ mock_init_combo.assert_called_once()
+ mock_init_le.assert_called_once()
+ mock_init_sb.assert_called_once()
+ mock_init_cat.assert_called_once()
+
+ @patch.object(DialogPreferences, "exec")
+ def test_on_category_changed_method_exists(self, mock_exec, mock_settings, qapp):
+ """Test that _on_category_changed method exists and is callable."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ assert hasattr(dialog, "_on_category_changed")
+ assert callable(dialog._on_category_changed)
+
+ # Test calling it doesn't raise an error
+ dialog._on_category_changed(0)
+ dialog._on_category_changed(1)
+ dialog._on_category_changed(2)
+ dialog._on_category_changed(3)
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
diff --git a/tests/test_settings_ui.py b/tests/test_settings_ui.py
new file mode 100644
index 0000000..8890500
--- /dev/null
+++ b/tests/test_settings_ui.py
@@ -0,0 +1,387 @@
+"""Tests for the refactored settings dialog UI."""
+
+from pathlib import Path
+from unittest.mock import Mock, patch
+
+import pytest
+from PySide6 import QtCore, 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
+ # Cleanup is handled by pytest-qt if available, otherwise by QApplication
+
+
+class TestUiDialogSettings:
+ """Test the generated UI class."""
+
+ def test_ui_setupui_creates_all_widgets(self, qapp):
+ """Test that setupUi creates all expected widgets."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Check main structural widgets
+ assert hasattr(ui, "lw_categories")
+ assert hasattr(ui, "sw_categories")
+ assert hasattr(ui, "bb_dialog")
+
+ # Check pages exist
+ assert hasattr(ui, "page_flags")
+ assert hasattr(ui, "page_quality")
+ assert hasattr(ui, "page_numbers")
+ assert hasattr(ui, "page_paths")
+
+ def test_ui_list_widget_properties(self, qapp):
+ """Test that the categories list widget has correct properties."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.lw_categories is not None
+ assert isinstance(ui.lw_categories, QtWidgets.QListWidget)
+ assert ui.lw_categories.minimumWidth() == 150
+ assert ui.lw_categories.maximumWidth() == 200
+
+ def test_ui_stacked_widget_has_all_pages(self, qapp):
+ """Test that the stacked widget contains all 4 pages."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.sw_categories is not None
+ assert isinstance(ui.sw_categories, QtWidgets.QStackedWidget)
+ assert ui.sw_categories.count() == 4
+
+ def test_ui_flags_page_widgets(self, qapp):
+ """Test that the Flags page contains all expected checkboxes."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Test flags group box exists
+ assert hasattr(ui, "gb_flags")
+ assert ui.gb_flags is not None
+
+ # Test all checkboxes exist
+ checkboxes = [
+ "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 cb_name in checkboxes:
+ assert hasattr(ui, cb_name), f"Missing checkbox: {cb_name}"
+ checkbox = getattr(ui, cb_name)
+ assert isinstance(checkbox, QtWidgets.QCheckBox)
+
+ def test_ui_quality_page_widgets(self, qapp):
+ """Test that the Quality page contains all expected combo boxes."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Test choices group box exists
+ assert hasattr(ui, "gb_choices")
+ assert ui.gb_choices is not None
+
+ # Test all combo boxes exist
+ combos = [
+ "c_quality_audio",
+ "c_quality_video",
+ "c_metadata_cover_dimension",
+ ]
+
+ for combo_name in combos:
+ assert hasattr(ui, combo_name), f"Missing combo box: {combo_name}"
+ combo = getattr(ui, combo_name)
+ assert isinstance(combo, QtWidgets.QComboBox)
+
+ def test_ui_numbers_page_widgets(self, qapp):
+ """Test that the Numbers page contains all expected spin boxes."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Test numbers group box exists
+ assert hasattr(ui, "gb_numbers")
+ assert ui.gb_numbers is not None
+
+ # Test all spin boxes exist
+ spinboxes = [
+ "sb_album_track_num_pad_min",
+ "sb_downloads_concurrent_max",
+ ]
+
+ for sb_name in spinboxes:
+ assert hasattr(ui, sb_name), f"Missing spin box: {sb_name}"
+ spinbox = getattr(ui, sb_name)
+ assert isinstance(spinbox, QtWidgets.QSpinBox)
+
+ def test_ui_paths_page_widgets(self, qapp):
+ """Test that the Paths page contains all expected line edits and buttons."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ # Test path group box exists
+ assert hasattr(ui, "gb_path")
+ assert ui.gb_path is not None
+
+ # Test all line edits exist
+ line_edits = [
+ "le_download_base_path",
+ "le_format_track",
+ "le_format_video",
+ "le_format_album",
+ "le_format_playlist",
+ "le_format_mix",
+ "le_path_binary_ffmpeg",
+ ]
+
+ for le_name in line_edits:
+ assert hasattr(ui, le_name), f"Missing line edit: {le_name}"
+ line_edit = getattr(ui, le_name)
+ assert isinstance(line_edit, QtWidgets.QLineEdit)
+
+ # Test buttons exist
+ assert hasattr(ui, "pb_download_base_path")
+ assert hasattr(ui, "pb_path_binary_ffmpeg")
+
+ def test_ui_dialog_button_box(self, qapp):
+ """Test that the dialog has OK and Cancel buttons."""
+ dialog = QtWidgets.QDialog()
+ ui = Ui_DialogSettings()
+ ui.setupUi(dialog)
+
+ assert ui.bb_dialog is not None
+ assert isinstance(ui.bb_dialog, QtWidgets.QDialogButtonBox)
+ # Check that standard buttons include OK and Cancel
+ buttons = ui.bb_dialog.standardButtons()
+ assert QtWidgets.QDialogButtonBox.StandardButton.Ok in buttons
+ assert QtWidgets.QDialogButtonBox.StandardButton.Cancel in buttons
+
+
+class TestDialogPreferencesCategories:
+ """Test the DialogPreferences category initialization and navigation."""
+
+ @pytest.fixture
+ def mock_settings(self):
+ """Create a mock Settings object."""
+ settings = Mock()
+ settings.data = Mock()
+
+ # Set default values for all parameters
+ settings.data.lyrics_embed = False
+ settings.data.lyrics_file = False
+ settings.data.use_primary_album_artist = False
+ settings.data.video_download = False
+ settings.data.download_dolby_atmos = False
+ settings.data.download_delay = False
+ settings.data.video_convert_mp4 = False
+ settings.data.extract_flac = False
+ settings.data.metadata_cover_embed = False
+ settings.data.mark_explicit = False
+ settings.data.cover_album_file = False
+ settings.data.skip_existing = False
+ settings.data.symlink_to_track = False
+ settings.data.playlist_create = False
+
+ settings.data.download_base_path = str(Path.home())
+ settings.data.format_album = "{artist}/{album}"
+ settings.data.format_playlist = "Playlists/{name}"
+ settings.data.format_mix = "Mixes/{name}"
+ settings.data.format_track = "{title}"
+ settings.data.format_video = "{title}"
+ settings.data.path_binary_ffmpeg = "ffmpeg"
+
+ settings.data.album_track_num_pad_min = 2
+ settings.data.downloads_concurrent_max = 3
+
+ settings.data.quality_audio = Mock()
+ settings.data.quality_video = Mock()
+ settings.data.metadata_cover_dimension = Mock()
+
+ return settings
+
+ @patch.object(DialogPreferences, "exec")
+ def test_init_categories_adds_all_items(self, mock_exec, mock_settings, qapp):
+ """Test that _init_categories adds all 4 categories to the list."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Check that all categories were added
+ assert dialog.ui.lw_categories.count() == 4
+ assert dialog.ui.lw_categories.item(0).text() == "Flags"
+ assert dialog.ui.lw_categories.item(1).text() == "Quality"
+ assert dialog.ui.lw_categories.item(2).text() == "Numbers"
+ assert dialog.ui.lw_categories.item(3).text() == "Paths & Formats"
+
+ @patch.object(DialogPreferences, "exec")
+ def test_init_categories_sets_first_selected(self, mock_exec, mock_settings, qapp):
+ """Test that the first category is selected by default."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Check that first item is selected
+ assert dialog.ui.lw_categories.currentRow() == 0
+
+ @patch.object(DialogPreferences, "exec")
+ def test_category_change_switches_page(self, mock_exec, mock_settings, qapp):
+ """Test that changing category switches the stacked widget page."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Initially on page 0
+ assert dialog.ui.sw_categories.currentIndex() == 0
+
+ # Change to category 1 (Quality)
+ dialog.ui.lw_categories.setCurrentRow(1)
+ assert dialog.ui.sw_categories.currentIndex() == 1
+
+ # Change to category 2 (Numbers)
+ dialog.ui.lw_categories.setCurrentRow(2)
+ assert dialog.ui.sw_categories.currentIndex() == 2
+
+ # Change to category 3 (Paths & Formats)
+ dialog.ui.lw_categories.setCurrentRow(3)
+ assert dialog.ui.sw_categories.currentIndex() == 3
+
+ # Back to category 0 (Flags)
+ dialog.ui.lw_categories.setCurrentRow(0)
+ assert dialog.ui.sw_categories.currentIndex() == 0
+
+ @patch.object(DialogPreferences, "exec")
+ def test_on_category_changed_method(self, mock_exec, mock_settings, qapp):
+ """Test that _on_category_changed method works correctly."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Test method directly
+ dialog._on_category_changed(0)
+ assert dialog.ui.sw_categories.currentIndex() == 0
+
+ dialog._on_category_changed(1)
+ assert dialog.ui.sw_categories.currentIndex() == 1
+
+ dialog._on_category_changed(2)
+ assert dialog.ui.sw_categories.currentIndex() == 2
+
+ dialog._on_category_changed(3)
+ assert dialog.ui.sw_categories.currentIndex() == 3
+
+
+class TestDialogPreferencesIntegration:
+ """Integration tests for the complete dialog functionality."""
+
+ @pytest.fixture
+ def mock_settings(self):
+ """Create a mock Settings object with all required attributes."""
+ settings = Mock()
+ settings.data = Mock()
+
+ # Set all required attributes
+ 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)
+
+ 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}")
+
+ settings.data.album_track_num_pad_min = 2
+ settings.data.downloads_concurrent_max = 3
+
+ settings.data.quality_audio = Mock()
+ settings.data.quality_video = Mock()
+ settings.data.metadata_cover_dimension = Mock()
+
+ return settings
+
+ @patch.object(DialogPreferences, "exec")
+ def test_dialog_structure_is_correct(self, mock_exec, mock_settings, qapp):
+ """Test that the complete dialog structure is correctly set up."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Check dialog has UI
+ assert hasattr(dialog, "ui")
+ assert isinstance(dialog.ui, Ui_DialogSettings)
+
+ # Check categories list is populated
+ assert dialog.ui.lw_categories.count() == 4
+
+ # Check stacked widget has 4 pages
+ assert dialog.ui.sw_categories.count() == 4
+
+ # Check all pages are accessible
+ for i in range(4):
+ page = dialog.ui.sw_categories.widget(i)
+ assert page is not None
+
+ @patch.object(DialogPreferences, "exec")
+ def test_navigation_between_all_categories(self, mock_exec, mock_settings, qapp):
+ """Test that navigation works correctly between all categories."""
+ settings_save = QtCore.Signal()
+
+ with patch.object(DialogPreferences, "gui_populate"):
+ dialog = DialogPreferences(mock_settings, settings_save)
+
+ # Navigate through all categories
+ for i in range(4):
+ dialog.ui.lw_categories.setCurrentRow(i)
+ assert dialog.ui.sw_categories.currentIndex() == i
+
+ # Check that the correct page is visible
+ current_widget = dialog.ui.sw_categories.currentWidget()
+ assert current_widget is not None