diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index cebbe7d..078b3ce 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -331,7 +331,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): } ) - async def validate_input(self, data: dict) -> dict[str]: + async def validate_input(self, data: dict) -> None: """Validate the user input allows us to connect. Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user. diff --git a/custom_components/versatile_thermostat/requirements_dev.txt b/custom_components/versatile_thermostat/requirements_dev.txt new file mode 100644 index 0000000..67296d7 --- /dev/null +++ b/custom_components/versatile_thermostat/requirements_dev.txt @@ -0,0 +1 @@ +homeassistant \ No newline at end of file diff --git a/custom_components/versatile_thermostat/requirements_test.txt b/custom_components/versatile_thermostat/requirements_test.txt new file mode 100644 index 0000000..4a1fb9a --- /dev/null +++ b/custom_components/versatile_thermostat/requirements_test.txt @@ -0,0 +1,3 @@ +# -r requirements_dev.txt +# aiodiscover +pytest-homeassistant-custom-component \ No newline at end of file diff --git a/custom_components/versatile_thermostat/tests/__init__.py b/custom_components/versatile_thermostat/tests/__init__.py new file mode 100644 index 0000000..2203450 --- /dev/null +++ b/custom_components/versatile_thermostat/tests/__init__.py @@ -0,0 +1 @@ +""" To make this repo a module """ diff --git a/custom_components/versatile_thermostat/tests/conftest.py b/custom_components/versatile_thermostat/tests/conftest.py new file mode 100644 index 0000000..01e5545 --- /dev/null +++ b/custom_components/versatile_thermostat/tests/conftest.py @@ -0,0 +1,52 @@ +"""Global fixtures for integration_blueprint integration.""" +# Fixtures allow you to replace functions with a Mock object. You can perform +# many options via the Mock to reflect a particular behavior from the original +# function that you want to see without going through the function's actual logic. +# Fixtures can either be passed into tests as parameters, or if autouse=True, they +# will automatically be used across all tests. +# +# Fixtures that are defined in conftest.py are available across all tests. You can also +# define fixtures within a particular test file to scope them locally. +# +# pytest_homeassistant_custom_component provides some fixtures that are provided by +# Home Assistant core. You can find those fixture definitions here: +# https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/blob/master/pytest_homeassistant_custom_component/common.py +# +# See here for more info: https://docs.pytest.org/en/latest/fixture.html (note that +# pytest includes fixtures OOB which you can use as defined on this page) +from unittest.mock import patch + +import pytest + +from custom_components.versatile_thermostat.config_flow import ( + VersatileThermostatBaseConfigFlow, +) + +pytest_plugins = "pytest_homeassistant_custom_component" + + +# This fixture enables loading custom integrations in all tests. +# Remove to enable selective use of this fixture +@pytest.fixture(autouse=True) +def auto_enable_custom_integrations(enable_custom_integrations): + yield + + +# This fixture is used to prevent HomeAssistant from attempting to create and dismiss persistent +# notifications. These calls would fail without this fixture since the persistent_notification +# integration is never loaded during a test. +@pytest.fixture(name="skip_notifications", autouse=True) +def skip_notifications_fixture(): + """Skip notification calls.""" + with patch("homeassistant.components.persistent_notification.async_create"), patch( + "homeassistant.components.persistent_notification.async_dismiss" + ): + yield + + +# This fixture is used to bypass the validate_input function in config_flow +@pytest.fixture(name="skip_validate_input") +def skip_validate_input_fixture(): + """Skip the validate_input in config flow""" + with patch.object(VersatileThermostatBaseConfigFlow, "validate_input"): + yield diff --git a/custom_components/versatile_thermostat/tests/const.py b/custom_components/versatile_thermostat/tests/const.py new file mode 100644 index 0000000..0b07e0d --- /dev/null +++ b/custom_components/versatile_thermostat/tests/const.py @@ -0,0 +1,71 @@ +from homeassistant.components.climate.const import ( + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, +) +from custom_components.versatile_thermostat.const import ( + CONF_NAME, + CONF_HEATER, + CONF_THERMOSTAT_CLIMATE, + CONF_THERMOSTAT_SWITCH, + CONF_THERMOSTAT_TYPE, + CONF_TEMP_SENSOR, + CONF_EXTERNAL_TEMP_SENSOR, + CONF_CYCLE_MIN, + CONF_TEMP_MAX, + CONF_TEMP_MIN, + CONF_PROP_FUNCTION, + PROPORTIONAL_FUNCTION_TPI, + CONF_TPI_COEF_INT, + CONF_TPI_COEF_EXT, + CONF_MINIMAL_ACTIVATION_DELAY, + CONF_SECURITY_DELAY_MIN, + CONF_SECURITY_MIN_ON_PERCENT, + CONF_SECURITY_DEFAULT_ON_PERCENT, + CONF_USE_WINDOW_FEATURE, + CONF_USE_MOTION_FEATURE, + CONF_USE_POWER_FEATURE, + CONF_USE_PRESENCE_FEATURE, +) + +MOCK_TH_OVER_SWITCH_USER_CONFIG = { + CONF_NAME: "TheOverSwitchMockName", + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, + CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", + CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", + CONF_CYCLE_MIN: 5, + CONF_TEMP_MIN: 15, + CONF_TEMP_MAX: 30, + # Keep all additional optional features to false +} + +MOCK_TH_OVER_SWITCH_TYPE_CONFIG = { + CONF_HEATER: "switch.mock_switch", + CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, +} + +MOCK_TH_OVER_SWITCH_TPI_CONFIG = { + CONF_TPI_COEF_INT: 0.3, + CONF_TPI_COEF_EXT: 0.1, +} + + +MOCK_PRESETS_CONFIG = { + PRESET_ECO + "_temp": 16, + PRESET_COMFORT + "_temp": 17, + PRESET_BOOST + "_temp": 18, +} + +MOCK_ADVANCED_CONFIG = { + CONF_MINIMAL_ACTIVATION_DELAY: 10, + CONF_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.4, + CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3, +} + +MOCK_DEFAULT_FEATURE_CONFIG = { + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, +} diff --git a/custom_components/versatile_thermostat/tests/test_config_flow.py b/custom_components/versatile_thermostat/tests/test_config_flow.py new file mode 100644 index 0000000..e0bc764 --- /dev/null +++ b/custom_components/versatile_thermostat/tests/test_config_flow.py @@ -0,0 +1,97 @@ +""" Test the Versatile Thermostat config flow """ + +from homeassistant import data_entry_flow +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import SOURCE_USER, ConfigEntry + +import pytest +from pytest_homeassistant_custom_component.common import MockConfigEntry, load_fixture + +from custom_components.versatile_thermostat.const import DOMAIN +from custom_components.versatile_thermostat import VersatileThermostatAPI + +from custom_components.versatile_thermostat.tests.const import ( + MOCK_TH_OVER_SWITCH_USER_CONFIG, + MOCK_TH_OVER_SWITCH_TYPE_CONFIG, + MOCK_TH_OVER_SWITCH_TPI_CONFIG, + MOCK_PRESETS_CONFIG, + MOCK_ADVANCED_CONFIG, + MOCK_DEFAULT_FEATURE_CONFIG, +) + + +async def test_show_form(hass: HomeAssistant) -> None: + """Test that the form is served with no input""" + # Init the API + # hass.data["custom_components"] = None + # loader.async_get_custom_components(hass) + # VersatileThermostatAPI(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + +async def test_user_config_flow_over_switch(hass, skip_validate_input): + """Test the config flow with thermostat_over_switch features""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_USER_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "type" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TYPE_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "tpi" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "presets" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_PRESETS_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "advanced" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_ADVANCED_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + result["data"] + == MOCK_TH_OVER_SWITCH_USER_CONFIG + | MOCK_TH_OVER_SWITCH_TYPE_CONFIG + | MOCK_TH_OVER_SWITCH_TPI_CONFIG + | MOCK_PRESETS_CONFIG + | MOCK_ADVANCED_CONFIG + | MOCK_DEFAULT_FEATURE_CONFIG + ) + assert result["result"] + assert result["result"].domain == DOMAIN + assert result["result"].version == 1 + assert result["result"].title == "TheOverSwitchMockName" + assert isinstance(result["result"], ConfigEntry) diff --git a/custom_components/versatile_thermostat/tests/test_unittest.py b/custom_components/versatile_thermostat/tests/test_unittest.py new file mode 100644 index 0000000..f217485 --- /dev/null +++ b/custom_components/versatile_thermostat/tests/test_unittest.py @@ -0,0 +1,14 @@ +import unittest # The test framework + + +class Test_TestIncrementDecrement(unittest.TestCase): + def test_increment(self): + self.assertEqual(4, 4) + + # This test is designed to fail for demonstration purposes. + def test_decrement(self): + self.assertEqual(3, 3) + + +if __name__ == "__main__": + unittest.main()