/
/
1"""Tests for the player-queues config-entry builders (config.py)."""
2
3from __future__ import annotations
4
5from collections.abc import Iterable
6from typing import TYPE_CHECKING
7from unittest.mock import Mock
8
9from music_assistant_models.enums import CrossfadeMode, ProviderFeature
10
11from music_assistant.constants import (
12 CONF_CROSSFADE_DURATION,
13 CONF_CROSSFADE_MODE,
14 CONF_VALUE_DISABLED,
15 CONF_VALUE_ENABLED,
16 CONF_VALUE_GLOBAL,
17 CONF_VOLUME_NORMALIZATION,
18)
19from music_assistant.controllers.player_queues.config import (
20 CATEGORY_DEFAULT_ENQUEUE_OPTION,
21 CATEGORY_ITEMS_TO_SELECT,
22 core_config_entries,
23 queue_config_entries,
24)
25from music_assistant.controllers.player_queues.constants import (
26 CONF_AUTOPLAY_MODE,
27 CONF_DEFAULT_ENQUEUE_OPTION_ALBUM,
28 CONF_DEFAULT_ENQUEUE_OPTION_TRACK,
29 CONF_DEFAULT_ENQUEUE_SELECT_ARTIST,
30 CONF_SMART_SHUFFLE_ENABLED,
31 CONF_SMART_SHUFFLE_SONG_RECENCY,
32)
33
34if TYPE_CHECKING:
35 from music_assistant_models.config_entries import ConfigEntry
36
37
38def _mass(*, similar_tracks: bool, smart_fades: bool) -> Mock:
39 """Build a mock MusicAssistant with the provider/smart-fades signals the builder reads."""
40 provider = Mock()
41 provider.supported_features = {ProviderFeature.SIMILAR_TRACKS} if similar_tracks else set()
42 mass = Mock()
43 mass.music.providers = [provider]
44 mass.streams.smart_fades_available = smart_fades
45 return mass
46
47
48def _by_key(entries: Iterable[ConfigEntry]) -> dict[str, ConfigEntry]:
49 return {entry.key: entry for entry in entries}
50
51
52def test_core_config_entries_enqueue_defaults() -> None:
53 """A track defaults to PLAY, other media types to REPLACE, in their tidied categories."""
54 by_key = _by_key(core_config_entries(_mass(similar_tracks=True, smart_fades=True)))
55 assert by_key[CONF_DEFAULT_ENQUEUE_OPTION_TRACK].default_value == "play"
56 assert by_key[CONF_DEFAULT_ENQUEUE_OPTION_ALBUM].default_value == "replace"
57 # the enqueue entries now live in their own dedicated categories
58 assert by_key[CONF_DEFAULT_ENQUEUE_SELECT_ARTIST].category == CATEGORY_ITEMS_TO_SELECT
59 assert by_key[CONF_DEFAULT_ENQUEUE_OPTION_TRACK].category == CATEGORY_DEFAULT_ENQUEUE_OPTION
60
61
62def test_core_config_entries_global_feature_defaults() -> None:
63 """Global (core) feature settings carry no 'global' option and keep the legacy defaults."""
64 by_key = _by_key(core_config_entries(_mass(similar_tracks=True, smart_fades=True)))
65 smart_shuffle = by_key[CONF_SMART_SHUFFLE_ENABLED]
66 assert smart_shuffle.default_value == CONF_VALUE_DISABLED
67 assert {opt.value for opt in smart_shuffle.options} == {CONF_VALUE_ENABLED, CONF_VALUE_DISABLED}
68 volume_normalization = by_key[CONF_VOLUME_NORMALIZATION]
69 assert volume_normalization.default_value == CONF_VALUE_ENABLED
70 assert CONF_VALUE_GLOBAL not in {opt.value for opt in volume_normalization.options}
71 # the global-only settings live only on the core schema
72 assert CONF_CROSSFADE_DURATION in by_key
73 assert CONF_SMART_SHUFFLE_SONG_RECENCY in by_key
74
75
76def test_queue_config_entries_offer_global_option() -> None:
77 """Each per-queue headline setting defaults to 'global'; global-only knobs are absent."""
78 by_key = _by_key(queue_config_entries(_mass(similar_tracks=True, smart_fades=True)))
79 for key in (
80 CONF_SMART_SHUFFLE_ENABLED,
81 CONF_VOLUME_NORMALIZATION,
82 CONF_CROSSFADE_MODE,
83 CONF_AUTOPLAY_MODE,
84 ):
85 entry = by_key[key]
86 assert entry.default_value == CONF_VALUE_GLOBAL
87 assert CONF_VALUE_GLOBAL in {opt.value for opt in entry.options}
88 # crossfade duration and the recency windows are global-only (not overridable per queue)
89 assert CONF_CROSSFADE_DURATION not in by_key
90 assert CONF_SMART_SHUFFLE_SONG_RECENCY not in by_key
91
92
93def test_queue_config_smart_crossfade_unavailable() -> None:
94 """Without smart fades the smart option is disabled and standard is the global default."""
95 mass = _mass(similar_tracks=True, smart_fades=False)
96 core = _by_key(core_config_entries(mass))
97 assert core[CONF_CROSSFADE_MODE].default_value == CrossfadeMode.STANDARD_CROSSFADE.value
98 queue_crossfade = _by_key(queue_config_entries(mass))[CONF_CROSSFADE_MODE]
99 smart = next(
100 opt for opt in queue_crossfade.options if opt.value == CrossfadeMode.SMART_CROSSFADE.value
101 )
102 assert smart.disabled is True
103
104
105def test_queue_config_smart_crossfade_available() -> None:
106 """With smart fades available, smart crossfade is the global default and stays enabled."""
107 mass = _mass(similar_tracks=True, smart_fades=True)
108 core = _by_key(core_config_entries(mass))
109 assert core[CONF_CROSSFADE_MODE].default_value == CrossfadeMode.SMART_CROSSFADE.value
110 queue_crossfade = _by_key(queue_config_entries(mass))[CONF_CROSSFADE_MODE]
111 smart = next(
112 opt for opt in queue_crossfade.options if opt.value == CrossfadeMode.SMART_CROSSFADE.value
113 )
114 assert smart.disabled is False
115
116
117def test_queue_config_similar_autoplay_disabled_without_provider() -> None:
118 """The 'similar' autoplay option is disabled when no provider can supply similar tracks."""
119 entries = queue_config_entries(_mass(similar_tracks=False, smart_fades=True))
120 autoplay = next(entry for entry in entries if entry.key == CONF_AUTOPLAY_MODE)
121 similar = next(opt for opt in autoplay.options if opt.value == "similar")
122 assert similar.disabled is True
123