From 610e9239a4397febf8bf192225a2d23629ff6c0a Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:06:03 -0400 Subject: [PATCH] Add media player test to Cambridge Audio (#125780) * Add media player tests to Cambridge Audio * Add media player tests to Cambridge Audio * Remove unnecessary test case * Move state_update call out of mock * Update tests/components/cambridge_audio/test_media_player.py --------- Co-authored-by: Joost Lekkerkerker --- tests/components/cambridge_audio/conftest.py | 17 +- tests/components/cambridge_audio/const.py | 6 + .../fixtures/get_now_playing.json | 25 +++ .../fixtures/get_play_state.json | 22 ++ .../cambridge_audio/fixtures/get_sources.json | 113 ++++++++++ .../cambridge_audio/fixtures/get_state.json | 7 + .../cambridge_audio/test_media_player.py | 193 ++++++++++++++++++ 7 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 tests/components/cambridge_audio/const.py create mode 100644 tests/components/cambridge_audio/fixtures/get_now_playing.json create mode 100644 tests/components/cambridge_audio/fixtures/get_play_state.json create mode 100644 tests/components/cambridge_audio/fixtures/get_sources.json create mode 100644 tests/components/cambridge_audio/fixtures/get_state.json create mode 100644 tests/components/cambridge_audio/test_media_player.py diff --git a/tests/components/cambridge_audio/conftest.py b/tests/components/cambridge_audio/conftest.py index 931c0f30af1..f17ff0cca3f 100644 --- a/tests/components/cambridge_audio/conftest.py +++ b/tests/components/cambridge_audio/conftest.py @@ -3,13 +3,13 @@ from collections.abc import Generator from unittest.mock import Mock, patch -from aiostreammagic.models import Info +from aiostreammagic.models import Info, NowPlaying, PlayState, Source, State import pytest from homeassistant.components.cambridge_audio.const import DOMAIN from homeassistant.const import CONF_HOST -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_fixture, load_json_array_fixture from tests.components.smhi.common import AsyncMock @@ -39,7 +39,20 @@ def mock_stream_magic_client() -> Generator[AsyncMock]: client = mock_client.return_value client.host = "192.168.20.218" client.info = Info.from_json(load_fixture("get_info.json", DOMAIN)) + client.sources = [ + Source.from_dict(x) + for x in load_json_array_fixture("get_sources.json", DOMAIN) + ] + client.state = State.from_json(load_fixture("get_state.json", DOMAIN)) + client.play_state = PlayState.from_json( + load_fixture("get_play_state.json", DOMAIN) + ) + client.now_playing = NowPlaying.from_json( + load_fixture("get_now_playing.json", DOMAIN) + ) client.is_connected = Mock(return_value=True) + client.position_last_updated = client.play_state.position + client.unregister_state_update_callbacks = AsyncMock(return_value=True) yield client diff --git a/tests/components/cambridge_audio/const.py b/tests/components/cambridge_audio/const.py new file mode 100644 index 00000000000..36057c79bb3 --- /dev/null +++ b/tests/components/cambridge_audio/const.py @@ -0,0 +1,6 @@ +"""Constants for Cambridge Audio integration tests.""" + +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN + +DEVICE_NAME = "cambridge_audio_cxnv2" +ENTITY_ID = f"{MP_DOMAIN}.{DEVICE_NAME}" diff --git a/tests/components/cambridge_audio/fixtures/get_now_playing.json b/tests/components/cambridge_audio/fixtures/get_now_playing.json new file mode 100644 index 00000000000..8dcc781be9b --- /dev/null +++ b/tests/components/cambridge_audio/fixtures/get_now_playing.json @@ -0,0 +1,25 @@ +{ + "state": "PLAYING", + "source": { + "id": "AIRPLAY", + "name": "AirPlay" + }, + "allow_apd": false, + "listening_on": "Listening on Cambridge Audio CXNv2 - AirPlay", + "display": { + "line1": "Holiday", + "line2": "Green Day", + "line3": "Greatest Hits: God's Favorite Band", + "format": "44.1kHz/16bit ALAC", + "mqa": "none", + "playback_source": "iPhone", + "class": "stream.service.airplay", + "art_file": "/tmp/current/AlbumArtFile-811-363", + "art_url": "http://192.168.20.218:80/album-art-2d89?id=1:246", + "progress": { + "position": 216, + "duration": 232 + } + }, + "controls": ["play_pause", "track_next", "track_previous"] +} diff --git a/tests/components/cambridge_audio/fixtures/get_play_state.json b/tests/components/cambridge_audio/fixtures/get_play_state.json new file mode 100644 index 00000000000..cd727ee58a7 --- /dev/null +++ b/tests/components/cambridge_audio/fixtures/get_play_state.json @@ -0,0 +1,22 @@ +{ + "state": "play", + "position": 179, + "presettable": false, + "mode_repeat": "off", + "mode_shuffle": "off", + "metadata": { + "class": "md.track", + "source": "AIRPLAY", + "name": "AirPlay", + "duration": 232, + "album": "Greatest Hits: God's Favorite Band", + "artist": "Green Day", + "title": "Holiday", + "art_url": "http://192.168.20.218:80/album-art-2d89?id=1:246", + "mqa": "none", + "codec": "ALAC", + "lossless": true, + "sample_rate": 44100, + "bit_depth": 16 + } +} diff --git a/tests/components/cambridge_audio/fixtures/get_sources.json b/tests/components/cambridge_audio/fixtures/get_sources.json new file mode 100644 index 00000000000..185f65e5ff6 --- /dev/null +++ b/tests/components/cambridge_audio/fixtures/get_sources.json @@ -0,0 +1,113 @@ +[ + { + "id": "IR", + "name": "Internet Radio", + "default_name": "Internet Radio", + "class": "stream.radio", + "nameable": false, + "ui_selectable": false, + "description": "Internet Radio", + "description_locale": "Internet Radio", + "preferred_order": 9 + }, + { + "id": "USB_AUDIO", + "name": "USB Audio", + "default_name": "USB Audio", + "class": "digital.usb", + "nameable": true, + "ui_selectable": true, + "description": "USB Audio", + "description_locale": "USB Audio", + "preferred_order": 1 + }, + { + "id": "SPDIF_COAX", + "name": "D2", + "default_name": "D2", + "class": "digital.coax", + "nameable": true, + "ui_selectable": false, + "description": "Digital Co-axial", + "description_locale": "Digital Co-axial", + "preferred_order": 3 + }, + { + "id": "SPDIF_TOSLINK", + "name": "D1", + "default_name": "D1", + "class": "digital.toslink", + "nameable": true, + "ui_selectable": false, + "description": "Digital Optical", + "description_locale": "Digital Optical", + "preferred_order": 2 + }, + { + "id": "MEDIA_PLAYER", + "name": "Media Library", + "default_name": "Media Library", + "class": "stream.media", + "nameable": false, + "ui_selectable": true, + "description": "Media Player", + "description_locale": "Media Player", + "preferred_order": 10 + }, + { + "id": "AIRPLAY", + "name": "AirPlay", + "default_name": "AirPlay", + "class": "stream.service.airplay", + "nameable": false, + "ui_selectable": true, + "description": "AirPlay", + "description_locale": "AirPlay", + "preferred_order": 11 + }, + { + "id": "SPOTIFY", + "name": "Spotify", + "default_name": "Spotify", + "class": "stream.service.spotify", + "nameable": false, + "ui_selectable": true, + "description": "Spotify", + "description_locale": "Spotify", + "preferred_order": 6, + "normalisation": "off" + }, + { + "id": "CAST", + "name": "Chromecast built-in", + "default_name": "Chromecast built-in", + "class": "stream.service.cast", + "nameable": false, + "ui_selectable": true, + "description": "Chromecast built-in", + "description_locale": "Chromecast built-in", + "preferred_order": 8 + }, + { + "id": "ROON", + "name": "Roon Ready", + "default_name": "Roon Ready", + "class": "stream.service.roon", + "nameable": false, + "ui_selectable": false, + "description": "Roon Ready", + "description_locale": "Roon Ready", + "preferred_order": 5 + }, + { + "id": "TIDAL", + "name": "TIDAL Connect", + "default_name": "TIDAL Connect", + "class": "stream.service.tidal", + "nameable": false, + "ui_selectable": false, + "description": "TIDAL", + "description_locale": "TIDAL", + "preferred_order": 7 + } +] diff --git a/tests/components/cambridge_audio/fixtures/get_state.json b/tests/components/cambridge_audio/fixtures/get_state.json new file mode 100644 index 00000000000..1acf0df4f6a --- /dev/null +++ b/tests/components/cambridge_audio/fixtures/get_state.json @@ -0,0 +1,7 @@ +{ + "source": "AIRPLAY", + "power": true, + "pre_amp_mode": false, + "pre_amp_state": "disabled_user", + "cbus": "off" +} diff --git a/tests/components/cambridge_audio/test_media_player.py b/tests/components/cambridge_audio/test_media_player.py new file mode 100644 index 00000000000..1f6564a6fab --- /dev/null +++ b/tests/components/cambridge_audio/test_media_player.py @@ -0,0 +1,193 @@ +"""Tests for the Cambridge Audio integration.""" + +from unittest.mock import AsyncMock + +from aiostreammagic import TransportControl +import pytest + +from homeassistant.components.media_player import ( + DOMAIN as MP_DOMAIN, + MediaPlayerEntityFeature, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + STATE_BUFFERING, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, + STATE_STANDBY, +) +from homeassistant.core import HomeAssistant + +from . import setup_integration +from .const import ENTITY_ID + +from tests.common import MockConfigEntry + + +async def mock_state_update(client: AsyncMock) -> None: + """Trigger a callback in the media player.""" + await client.register_state_update_callbacks.call_args[0][0](client) + + +async def test_entity_supported_features( + hass: HomeAssistant, + mock_stream_magic_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test entity attributes.""" + await setup_integration(hass, mock_config_entry) + await mock_state_update(mock_stream_magic_client) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + attrs = state.attributes + + # Ensure volume isn't available when pre-amp is disabled + assert not mock_stream_magic_client.state.pre_amp_mode + assert ( + MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.VOLUME_MUTE + not in attrs[ATTR_SUPPORTED_FEATURES] + ) + + # Check for basic media controls + assert { + TransportControl.PLAY_PAUSE, + TransportControl.TRACK_NEXT, + TransportControl.TRACK_PREVIOUS, + }.issubset(mock_stream_magic_client.now_playing.controls) + assert ( + MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.PREVIOUS_TRACK + in attrs[ATTR_SUPPORTED_FEATURES] + ) + assert ( + MediaPlayerEntityFeature.SHUFFLE_SET + | MediaPlayerEntityFeature.REPEAT_SET + | MediaPlayerEntityFeature.SEEK + not in attrs[ATTR_SUPPORTED_FEATURES] + ) + + mock_stream_magic_client.now_playing.controls = [ + TransportControl.TOGGLE_REPEAT, + TransportControl.TOGGLE_SHUFFLE, + TransportControl.SEEK, + ] + await mock_state_update(mock_stream_magic_client) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + attrs = state.attributes + + assert ( + MediaPlayerEntityFeature.SHUFFLE_SET + | MediaPlayerEntityFeature.REPEAT_SET + | MediaPlayerEntityFeature.SEEK + in attrs[ATTR_SUPPORTED_FEATURES] + ) + + mock_stream_magic_client.state.pre_amp_mode = True + await mock_state_update(mock_stream_magic_client) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + attrs = state.attributes + assert ( + MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.VOLUME_MUTE + in attrs[ATTR_SUPPORTED_FEATURES] + ) + + +@pytest.mark.parametrize( + ("power_state", "play_state", "media_player_state"), + [ + (True, "NETWORK", STATE_STANDBY), + (False, "NETWORK", STATE_STANDBY), + (False, "play", STATE_OFF), + (True, "play", STATE_PLAYING), + (True, "pause", STATE_PAUSED), + (True, "connecting", STATE_BUFFERING), + (True, "stop", STATE_IDLE), + (True, "ready", STATE_IDLE), + ], +) +async def test_entity_state( + hass: HomeAssistant, + mock_stream_magic_client: AsyncMock, + mock_config_entry: MockConfigEntry, + power_state: bool, + play_state: str, + media_player_state: str, +) -> None: + """Test media player state.""" + await setup_integration(hass, mock_config_entry) + mock_stream_magic_client.state.power = power_state + mock_stream_magic_client.play_state.state = play_state + await mock_state_update(mock_stream_magic_client) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == media_player_state + + +async def test_media_play_pause_stop( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_stream_magic_client: AsyncMock, +) -> None: + """Test media next previous track service.""" + await setup_integration(hass, mock_config_entry) + + data = {ATTR_ENTITY_ID: ENTITY_ID} + + # Test for play/pause command when separate play and pause controls are unavailable + await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PAUSE, data, True) + mock_stream_magic_client.play_pause.assert_called_once() + + await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PLAY, data, True) + assert mock_stream_magic_client.play_pause.call_count == 2 + + # Test for separate play and pause controls + mock_stream_magic_client.now_playing.controls = [ + TransportControl.PLAY, + TransportControl.PAUSE, + ] + await mock_state_update(mock_stream_magic_client) + await hass.async_block_till_done() + + await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PAUSE, data, True) + mock_stream_magic_client.pause.assert_called_once() + + await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PLAY, data, True) + mock_stream_magic_client.play.assert_called_once() + + +async def test_media_next_previous_track( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_stream_magic_client: AsyncMock, +) -> None: + """Test media next previous track service.""" + await setup_integration(hass, mock_config_entry) + + data = {ATTR_ENTITY_ID: ENTITY_ID} + + await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data, True) + + mock_stream_magic_client.next_track.assert_called_once() + + await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data, True) + + mock_stream_magic_client.previous_track.assert_called_once()