From 7bad7fc4f6a36a0f69e4dea8f0056eeb05cd6e90 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:53:11 -0400 Subject: [PATCH] Update template button tests to use new framework (#167806) --- tests/components/template/test_button.py | 373 ++++++++++++----------- 1 file changed, 189 insertions(+), 184 deletions(-) diff --git a/tests/components/template/test_button.py b/tests/components/template/test_button.py index 77d316ce89d..8eb654b848a 100644 --- a/tests/components/template/test_button.py +++ b/tests/components/template/test_button.py @@ -7,10 +7,8 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant import setup from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.template import DOMAIN -from homeassistant.components.template.button import DEFAULT_NAME from homeassistant.components.template.const import CONF_PICTURE from homeassistant.const import ( ATTR_ENTITY_PICTURE, @@ -19,15 +17,72 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_ICON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er -from tests.common import MockConfigEntry, assert_setup_component +from .conftest import ( + ConfigurationStyle, + TemplatePlatformSetup, + assert_action, + async_trigger, + make_test_action, + setup_and_test_nested_unique_id, + setup_and_test_unique_id, + setup_entity, +) -_TEST_BUTTON = "button.template_button" -_TEST_OPTIONS_BUTTON = "button.test" +from tests.common import MockConfigEntry + +TEST_ATTRIBUTE_ENTITY_ID = "sensor.test_attribute" +TEST_AVAILABILITY_ENTITY = "binary_sensor.availability" +TEST_BUTTON = TemplatePlatformSetup(BUTTON_DOMAIN, None, "template_button", {}) +PRESS_ACTION = make_test_action("press") + + +@pytest.fixture +async def setup_button(hass: HomeAssistant, count: int, config: dict[str, Any]) -> None: + """Do setup of button integration.""" + await setup_entity(hass, TEST_BUTTON, ConfigurationStyle.MODERN, count, config) + + +@pytest.fixture +async def setup_single_attribute_button( + hass: HomeAssistant, + attribute: str, + attribute_template: str, + config: dict, +) -> None: + """Do setup of button integration with a single attribute.""" + await setup_entity( + hass, + TEST_BUTTON, + ConfigurationStyle.MODERN, + 1, + config, + extra_config={attribute: attribute_template} + if attribute and attribute_template + else {}, + ) + + +def _verify( + hass: HomeAssistant, + expected_value: str, + attributes: dict[str, Any] | None = None, + entity_id: str = TEST_BUTTON.entity_id, +) -> None: + """Verify button's state.""" + attributes = attributes or {} + if CONF_FRIENDLY_NAME not in attributes: + attributes[CONF_FRIENDLY_NAME] = TEST_BUTTON.object_id + state = hass.states.get(entity_id) + assert state.state == expected_value + assert state.attributes == attributes @pytest.mark.parametrize( @@ -74,50 +129,20 @@ async def test_setup_config_entry( assert state == snapshot +@pytest.mark.parametrize(("count", "config"), [(1, PRESS_ACTION)]) +@pytest.mark.usefixtures("setup_button") async def test_missing_optional_config(hass: HomeAssistant) -> None: """Test: missing optional template is ok.""" - with assert_setup_component(1, "template"): - assert await setup.async_setup_component( - hass, - "template", - { - "template": { - "button": { - "press": {"service": "script.press"}, - }, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - _verify(hass, STATE_UNKNOWN) +@pytest.mark.parametrize(("count", "config"), [(1, {"press": []})]) +@pytest.mark.usefixtures("setup_button") async def test_missing_emtpy_press_action_config( hass: HomeAssistant, freezer: FrozenDateTimeFactory, ) -> None: """Test: missing optional template is ok.""" - with assert_setup_component(1, "template"): - assert await setup.async_setup_component( - hass, - "template", - { - "template": { - "button": { - "press": [], - }, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - _verify(hass, STATE_UNKNOWN) now = dt.datetime.now(dt.UTC) @@ -125,7 +150,7 @@ async def test_missing_emtpy_press_action_config( await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {CONF_ENTITY_ID: _TEST_BUTTON}, + {CONF_ENTITY_ID: TEST_BUTTON.entity_id}, blocking=True, ) @@ -135,63 +160,40 @@ async def test_missing_emtpy_press_action_config( ) +@pytest.mark.parametrize(("count", "config"), [(0, {})]) +@pytest.mark.usefixtures("setup_button") async def test_missing_required_keys(hass: HomeAssistant) -> None: """Test: missing required fields will fail.""" - with assert_setup_component(0, "template"): - assert await setup.async_setup_component( - hass, - "template", - {"template": {"button": {}}}, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - assert hass.states.async_all("button") == [] -async def test_all_optional_config( +@pytest.mark.parametrize( + ("count", "config"), + [ + ( + 1, + { + **PRESS_ACTION, + "device_class": "restart", + }, + ) + ], +) +@pytest.mark.usefixtures("setup_button") +async def test_device_class_option( hass: HomeAssistant, - entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, calls: list[ServiceCall], ) -> None: - """Test: including all optional templates is ok.""" - with assert_setup_component(1, "template"): - assert await setup.async_setup_component( - hass, - "template", - { - "template": { - "unique_id": "test", - "button": { - "press": { - "service": "test.automation", - "data_template": {"caller": "{{ this.entity_id }}"}, - }, - "device_class": "restart", - "unique_id": "test", - "name": "test", - "icon": "mdi:test", - }, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - + """Test optional options is ok.""" _verify( hass, STATE_UNKNOWN, { CONF_DEVICE_CLASS: "restart", - CONF_FRIENDLY_NAME: "test", - CONF_ICON: "mdi:test", + CONF_FRIENDLY_NAME: TEST_BUTTON.object_id, }, - _TEST_OPTIONS_BUTTON, + TEST_BUTTON.entity_id, ) now = dt.datetime.now(dt.UTC) @@ -199,47 +201,77 @@ async def test_all_optional_config( await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {CONF_ENTITY_ID: _TEST_OPTIONS_BUTTON}, + {CONF_ENTITY_ID: TEST_BUTTON.entity_id}, blocking=True, ) - assert len(calls) == 1 - assert calls[0].data["caller"] == _TEST_OPTIONS_BUTTON - + assert_action(TEST_BUTTON, calls, 1, "press") _verify( hass, now.isoformat(), { CONF_DEVICE_CLASS: "restart", - CONF_FRIENDLY_NAME: "test", - CONF_ICON: "mdi:test", + CONF_FRIENDLY_NAME: TEST_BUTTON.object_id, }, - _TEST_OPTIONS_BUTTON, + TEST_BUTTON.entity_id, ) - assert entity_registry.async_get_entity_id("button", "template", "test-test") + +@pytest.mark.parametrize("config", [PRESS_ACTION]) +@pytest.mark.parametrize( + ("attribute", "attribute_template", "attribute_name", "expected"), + [ + ( + CONF_ICON, + "{{ 'mdi:test' if is_state('sensor.test_attribute', 'on') else '' }}", + ATTR_ICON, + "mdi:test", + ), + ( + CONF_PICTURE, + "{{ 'test.jpg' if is_state('sensor.test_attribute', 'on') else '' }}", + ATTR_ENTITY_PICTURE, + "test.jpg", + ), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_button") +async def test_options_that_are_templates( + hass: HomeAssistant, + attribute_name: str, + expected: str, + freezer: FrozenDateTimeFactory, + calls: list[ServiceCall], +) -> None: + """Test button options that are templates.""" + expected_attributes = {attribute_name: expected} + + _verify(hass, STATE_UNKNOWN, {attribute_name: ""}) + + await async_trigger(hass, TEST_ATTRIBUTE_ENTITY_ID, STATE_ON) + + _verify(hass, STATE_UNKNOWN, expected_attributes) + + now = dt.datetime.now(dt.UTC) + freezer.move_to(now) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {CONF_ENTITY_ID: TEST_BUTTON.entity_id}, + blocking=True, + ) + + assert_action(TEST_BUTTON, calls, 1, "press") + _verify(hass, now.isoformat(), expected_attributes) +@pytest.mark.parametrize("config", [PRESS_ACTION]) +@pytest.mark.parametrize( + ("attribute", "attribute_template"), [("name", "Button {{ 1 + 1 }}")] +) +@pytest.mark.usefixtures("setup_single_attribute_button") async def test_name_template(hass: HomeAssistant) -> None: """Test: name template.""" - with assert_setup_component(1, "template"): - assert await setup.async_setup_component( - hass, - "template", - { - "template": { - "button": { - "press": {"service": "script.press"}, - "name": "Button {{ 1 + 1 }}", - }, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - _verify( hass, STATE_UNKNOWN, @@ -250,86 +282,20 @@ async def test_name_template(hass: HomeAssistant) -> None: ) -@pytest.mark.parametrize( - ("field", "attribute", "test_template", "expected"), - [ - (CONF_ICON, ATTR_ICON, "mdi:test{{ 1 + 1 }}", "mdi:test2"), - (CONF_PICTURE, ATTR_ENTITY_PICTURE, "test{{ 1 + 1 }}.jpg", "test2.jpg"), - ], -) -async def test_templated_optional_config( - hass: HomeAssistant, - field: str, - attribute: str, - test_template: str, - expected: str, -) -> None: - """Test optional config templates.""" - with assert_setup_component(1, "template"): - assert await setup.async_setup_component( - hass, - "template", - { - "template": { - "button": { - "press": {"service": "script.press"}, - field: test_template, - }, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - _verify( - hass, - STATE_UNKNOWN, - { - attribute: expected, - }, - "button.template_button", +async def test_unique_id(hass: HomeAssistant) -> None: + """Test unique_id option only creates one button per id.""" + await setup_and_test_unique_id( + hass, TEST_BUTTON, ConfigurationStyle.MODERN, PRESS_ACTION ) -async def test_unique_id(hass: HomeAssistant) -> None: - """Test: unique id is ok.""" - with assert_setup_component(1, "template"): - assert await setup.async_setup_component( - hass, - "template", - { - "template": { - "unique_id": "test", - "button": { - "press": {"service": "script.press"}, - "unique_id": "test", - }, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - _verify(hass, STATE_UNKNOWN) - - -def _verify( - hass: HomeAssistant, - expected_value: str, - attributes: dict[str, Any] | None = None, - entity_id: str = _TEST_BUTTON, +async def test_nested_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: - """Verify button's state.""" - attributes = attributes or {} - if CONF_FRIENDLY_NAME not in attributes: - attributes[CONF_FRIENDLY_NAME] = DEFAULT_NAME - state = hass.states.get(entity_id) - assert state.state == expected_value - assert state.attributes == attributes + """Test a template unique_id propagates to button unique_ids.""" + await setup_and_test_nested_unique_id( + hass, TEST_BUTTON, ConfigurationStyle.MODERN, entity_registry, PRESS_ACTION + ) async def test_device_id( @@ -376,3 +342,42 @@ async def test_device_id( template_entity = entity_registry.async_get("button.my_template") assert template_entity is not None assert template_entity.device_id == device_entry.id + + +@pytest.mark.parametrize( + ("config", "attribute", "attribute_template"), + [ + ( + PRESS_ACTION, + "availability", + "{{ is_state('binary_sensor.availability', 'on') }}", + ) + ], +) +@pytest.mark.usefixtures("setup_single_attribute_button") +async def test_available_template_with_entities(hass: HomeAssistant) -> None: + """Test availability templates with values from other entities.""" + + await async_trigger(hass, TEST_AVAILABILITY_ENTITY, STATE_ON) + + # Device State should not be unavailable + assert hass.states.get(TEST_BUTTON.entity_id).state != STATE_UNAVAILABLE + + # When Availability template returns false + await async_trigger(hass, TEST_AVAILABILITY_ENTITY, STATE_OFF) + + # device state should be unavailable + assert hass.states.get(TEST_BUTTON.entity_id).state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ("config", "attribute", "attribute_template"), + [(PRESS_ACTION, "availability", "{{ x - 12 }}")], +) +@pytest.mark.usefixtures("setup_single_attribute_button") +async def test_invalid_availability_template_keeps_component_available( + hass: HomeAssistant, caplog_setup_text +) -> None: + """Test that an invalid availability keeps the device available.""" + assert hass.states.get(TEST_BUTTON.entity_id).state != STATE_UNAVAILABLE + assert "UndefinedError: 'x' is undefined" in caplog_setup_text