From a16ea3d7bdd57bbd0fe3402ac4f2999c451dcc7b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 14 Mar 2024 14:04:41 +0100 Subject: [PATCH] Deprecate the map integration (#113215) * Deprecate the map integration * Revert changes in DashboardsCollection._async_load_data * Add option to allow single word in dashboard URL * Update tests * Translate title * Add icon * Improve test coverage --- .coveragerc | 1 - homeassistant/components/lovelace/__init__.py | 35 +++++- homeassistant/components/lovelace/const.py | 3 + .../components/lovelace/dashboard.py | 9 +- .../components/lovelace/manifest.json | 1 + homeassistant/components/map/__init__.py | 46 ++++++- homeassistant/components/map/manifest.json | 2 +- .../components/onboarding/__init__.py | 53 ++++++-- .../components/onboarding/strings.json | 3 + homeassistant/components/onboarding/views.py | 15 ++- script/hassfest/translations.py | 5 +- tests/components/lovelace/test_cast.py | 16 ++- tests/components/lovelace/test_dashboard.py | 18 ++- tests/components/lovelace/test_init.py | 98 +++++++++++++++ .../components/lovelace/test_system_health.py | 18 ++- tests/components/map/__init__.py | 1 + tests/components/map/test_init.py | 116 ++++++++++++++++++ tests/components/onboarding/test_init.py | 13 +- tests/components/onboarding/test_views.py | 63 +++++++++- 19 files changed, 480 insertions(+), 36 deletions(-) create mode 100644 tests/components/lovelace/test_init.py create mode 100644 tests/components/map/__init__.py create mode 100644 tests/components/map/test_init.py diff --git a/.coveragerc b/.coveragerc index a60f216af1b0fc..54d543dfbf3937 100644 --- a/.coveragerc +++ b/.coveragerc @@ -747,7 +747,6 @@ omit = homeassistant/components/lyric/climate.py homeassistant/components/lyric/sensor.py homeassistant/components/mailgun/notify.py - homeassistant/components/map/* homeassistant/components/mastodon/notify.py homeassistant/components/matrix/__init__.py homeassistant/components/matrix/notify.py diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 73be119880f35c..60d03717be0cc2 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -4,7 +4,7 @@ import voluptuous as vol -from homeassistant.components import frontend, websocket_api +from homeassistant.components import frontend, onboarding, websocket_api from homeassistant.config import ( async_hass_config_yaml, async_process_component_and_handle_errors, @@ -14,11 +14,13 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from . import dashboard, resources, websocket from .const import ( # noqa: F401 + CONF_ALLOW_SINGLE_WORD, CONF_ICON, CONF_REQUIRE_ADMIN, CONF_SHOW_IN_SIDEBAR, @@ -201,6 +203,9 @@ async def storage_dashboard_changed(change_type, item_id, item): # Process storage dashboards dashboards_collection = dashboard.DashboardsCollection(hass) + # This can be removed when the map integration is removed + hass.data[DOMAIN]["dashboards_collection"] = dashboards_collection + dashboards_collection.async_add_listener(storage_dashboard_changed) await dashboards_collection.async_load() @@ -212,6 +217,12 @@ async def storage_dashboard_changed(change_type, item_id, item): STORAGE_DASHBOARD_UPDATE_FIELDS, ).async_setup(hass, create_list=False) + def create_map_dashboard(): + hass.async_create_task(_create_map_dashboard(hass)) + + if not onboarding.async_is_onboarded(hass): + onboarding.async_add_listener(hass, create_map_dashboard) + return True @@ -249,3 +260,25 @@ def _register_panel(hass, url_path, mode, config, update): kwargs["sidebar_icon"] = config.get(CONF_ICON, DEFAULT_ICON) frontend.async_register_built_in_panel(hass, DOMAIN, **kwargs) + + +async def _create_map_dashboard(hass: HomeAssistant): + translations = await async_get_translations( + hass, hass.config.language, "dashboard", {onboarding.DOMAIN} + ) + title = translations["component.onboarding.dashboard.map.title"] + + dashboards_collection: dashboard.DashboardsCollection = hass.data[DOMAIN][ + "dashboards_collection" + ] + await dashboards_collection.async_create_item( + { + CONF_ALLOW_SINGLE_WORD: True, + CONF_ICON: "mdi:map", + CONF_TITLE: title, + CONF_URL_PATH: "map", + } + ) + + map_store: dashboard.LovelaceStorage = hass.data[DOMAIN]["dashboards"]["map"] + await map_store.async_save({"strategy": {"type": "map"}}) diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py index 0f1e818e8bd857..538bd49d72cd8e 100644 --- a/homeassistant/components/lovelace/const.py +++ b/homeassistant/components/lovelace/const.py @@ -24,6 +24,7 @@ MODE_AUTO = "auto-gen" LOVELACE_CONFIG_FILE = "ui-lovelace.yaml" +CONF_ALLOW_SINGLE_WORD = "allow_single_word" CONF_URL_PATH = "url_path" CONF_RESOURCE_TYPE_WS = "res_type" @@ -75,6 +76,8 @@ # For now we write "storage" as all modes. # In future we can adjust this to be other modes. vol.Optional(CONF_MODE, default=MODE_STORAGE): MODE_STORAGE, + # Set to allow adding dashboard without hyphen + vol.Optional(CONF_ALLOW_SINGLE_WORD): bool, } STORAGE_DASHBOARD_UPDATE_FIELDS = DASHBOARD_BASE_UPDATE_FIELDS diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index 98c03e20d87437..17116a011a4fd3 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -18,6 +18,7 @@ from homeassistant.util.yaml import Secrets, load_yaml_dict from .const import ( + CONF_ALLOW_SINGLE_WORD, CONF_ICON, CONF_URL_PATH, DOMAIN, @@ -234,10 +235,14 @@ def __init__(self, hass): async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - if "-" not in data[CONF_URL_PATH]: + url_path = data[CONF_URL_PATH] + + allow_single_word = data.pop(CONF_ALLOW_SINGLE_WORD, False) + + if not allow_single_word and "-" not in url_path: raise vol.Invalid("Url path needs to contain a hyphen (-)") - if data[CONF_URL_PATH] in self.hass.data[DATA_PANELS]: + if url_path in self.hass.data[DATA_PANELS]: raise vol.Invalid("Panel url path needs to be unique") return self.CREATE_SCHEMA(data) diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index 9dcffdb3b6cf31..ed55142ee773f5 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -2,6 +2,7 @@ "domain": "lovelace", "name": "Dashboards", "codeowners": ["@home-assistant/frontend"], + "dependencies": ["onboarding"], "documentation": "https://www.home-assistant.io/integrations/lovelace", "integration_type": "system", "quality_scale": "internal" diff --git a/homeassistant/components/map/__init__.py b/homeassistant/components/map/__init__.py index a78199ad6607c2..beddde5df1d441 100644 --- a/homeassistant/components/map/__init__.py +++ b/homeassistant/components/map/__init__.py @@ -1,16 +1,52 @@ """Support for showing device locations.""" - -from homeassistant.components import frontend -from homeassistant.core import HomeAssistant +from homeassistant.components import onboarding +from homeassistant.components.lovelace import _create_map_dashboard +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType DOMAIN = "map" CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) +STORAGE_KEY = DOMAIN +STORAGE_VERSION_MAJOR = 1 + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Register the built-in map panel.""" - frontend.async_register_built_in_panel(hass, "map", "map", "hass:tooltip-account") + """Create a map panel.""" + + if DOMAIN in config: + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "map", + }, + ) + + store: Store[dict[str, bool]] = Store( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + ) + data = await store.async_load() + if data: + return True + + if onboarding.async_is_onboarded(hass): + await _create_map_dashboard(hass) + + await store.async_save({"migrated": True}) + return True diff --git a/homeassistant/components/map/manifest.json b/homeassistant/components/map/manifest.json index b617aa3e5faf57..6a0333c862a053 100644 --- a/homeassistant/components/map/manifest.json +++ b/homeassistant/components/map/manifest.json @@ -2,7 +2,7 @@ "domain": "map", "name": "Map", "codeowners": [], - "dependencies": ["frontend"], + "dependencies": ["frontend", "lovelace"], "documentation": "https://www.home-assistant.io/integrations/map", "integration_type": "system", "quality_scale": "internal" diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 61576c831bf4b5..c11bd79c377faa 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -2,7 +2,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -26,15 +28,30 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) -class OnboadingStorage(Store[dict[str, list[str]]]): +@dataclass +class OnboardingData: + """Container for onboarding data.""" + + listeners: list[Callable[[], None]] + onboarded: bool + steps: OnboardingStoreData + + +class OnboardingStoreData(TypedDict): + """Onboarding store data.""" + + done: list[str] + + +class OnboardingStorage(Store[OnboardingStoreData]): """Store onboarding data.""" async def _async_migrate_func( self, old_major_version: int, old_minor_version: int, - old_data: dict[str, list[str]], - ) -> dict[str, list[str]]: + old_data: OnboardingStoreData, + ) -> OnboardingStoreData: """Migrate to the new version.""" # From version 1 -> 2, we automatically mark the integration step done if old_major_version < 2: @@ -50,21 +67,37 @@ async def _async_migrate_func( @callback def async_is_onboarded(hass: HomeAssistant) -> bool: """Return if Home Assistant has been onboarded.""" - data = hass.data.get(DOMAIN) - return data is None or data is True + data: OnboardingData | None = hass.data.get(DOMAIN) + return data is None or data.onboarded is True @bind_hass @callback def async_is_user_onboarded(hass: HomeAssistant) -> bool: """Return if a user has been created as part of onboarding.""" - return async_is_onboarded(hass) or STEP_USER in hass.data[DOMAIN]["done"] + return async_is_onboarded(hass) or STEP_USER in hass.data[DOMAIN].steps["done"] + + +@callback +def async_add_listener(hass: HomeAssistant, listener: Callable[[], None]) -> None: + """Add a listener to be called when onboarding is complete.""" + data: OnboardingData | None = hass.data.get(DOMAIN) + + if not data: + # Onboarding not active + return + + if data.onboarded: + listener() + return + + data.listeners.append(listener) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the onboarding component.""" - store = OnboadingStorage(hass, STORAGE_VERSION, STORAGE_KEY, private=True) - data: dict[str, list[str]] | None + store = OnboardingStorage(hass, STORAGE_VERSION, STORAGE_KEY, private=True) + data: OnboardingStoreData | None if (data := await store.async_load()) is None: data = {"done": []} @@ -88,7 +121,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if set(data["done"]) == set(STEPS): return True - hass.data[DOMAIN] = data + hass.data[DOMAIN] = OnboardingData([], False, data) await views.async_setup(hass, data, store) diff --git a/homeassistant/components/onboarding/strings.json b/homeassistant/components/onboarding/strings.json index 9e3806927d28f0..bc8bb6e54ff468 100644 --- a/homeassistant/components/onboarding/strings.json +++ b/homeassistant/components/onboarding/strings.json @@ -3,5 +3,8 @@ "living_room": "Living Room", "bedroom": "Bedroom", "kitchen": "Kitchen" + }, + "dashboard": { + "map": { "title": "Map" } } } diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index bcc4bd26a588be..bb4d84d1b50491 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -26,7 +26,7 @@ from homeassistant.util.async_ import create_eager_task if TYPE_CHECKING: - from . import OnboadingStorage + from . import OnboardingData, OnboardingStorage, OnboardingStoreData from .const import ( DEFAULT_AREAS, @@ -40,7 +40,7 @@ async def async_setup( - hass: HomeAssistant, data: dict[str, list[str]], store: OnboadingStorage + hass: HomeAssistant, data: OnboardingStoreData, store: OnboardingStorage ) -> None: """Set up the onboarding view.""" hass.http.register_view(OnboardingView(data, store)) @@ -58,7 +58,7 @@ class OnboardingView(HomeAssistantView): url = "/api/onboarding" name = "api:onboarding" - def __init__(self, data: dict[str, list[str]], store: OnboadingStorage) -> None: + def __init__(self, data: OnboardingStoreData, store: OnboardingStorage) -> None: """Initialize the onboarding view.""" self._store = store self._data = data @@ -77,7 +77,7 @@ class InstallationTypeOnboardingView(HomeAssistantView): url = "/api/onboarding/installation_type" name = "api:onboarding:installation_type" - def __init__(self, data: dict[str, list[str]]) -> None: + def __init__(self, data: OnboardingStoreData) -> None: """Initialize the onboarding installation type view.""" self._data = data @@ -96,7 +96,7 @@ class _BaseOnboardingView(HomeAssistantView): step: str - def __init__(self, data: dict[str, list[str]], store: OnboadingStorage) -> None: + def __init__(self, data: OnboardingStoreData, store: OnboardingStorage) -> None: """Initialize the onboarding view.""" self._store = store self._data = data @@ -113,7 +113,10 @@ async def _async_mark_done(self, hass: HomeAssistant) -> None: await self._store.async_save(self._data) if set(self._data["done"]) == set(STEPS): - hass.data[DOMAIN] = True + data: OnboardingData = hass.data[DOMAIN] + data.onboarded = True + for listener in data.listeners: + listener() class UserOnboardingView(_BaseOnboardingView): diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index f595776febea7e..724f65eafb62e2 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -441,7 +441,10 @@ def device_class_validator(value: str) -> str: ONBOARDING_SCHEMA = vol.Schema( - {vol.Required("area"): {str: translation_value_validator}} + { + vol.Required("area"): {str: translation_value_validator}, + vol.Required("dashboard"): {str: {"title": translation_value_validator}}, + } ) diff --git a/tests/components/lovelace/test_cast.py b/tests/components/lovelace/test_cast.py index 12cf4a84095d55..5fdce538d4ca0a 100644 --- a/tests/components/lovelace/test_cast.py +++ b/tests/components/lovelace/test_cast.py @@ -1,7 +1,8 @@ """Test the Lovelace Cast platform.""" +from collections.abc import Generator from time import time -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -15,6 +16,19 @@ from tests.common import async_mock_service +@pytest.fixture(autouse=True) +def mock_onboarding_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding. + + Enabled to prevent creating default dashboards during test execution. + """ + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ) as mock_onboarding: + yield mock_onboarding + + @pytest.fixture async def mock_https_url(hass): """Mock valid URL.""" diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index a85770d8ae42d4..d0ac6e988359bd 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -1,7 +1,8 @@ """Test the Lovelace initialization.""" +from collections.abc import Generator from typing import Any -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -14,6 +15,19 @@ from tests.typing import WebSocketGenerator +@pytest.fixture(autouse=True) +def mock_onboarding_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding. + + Enabled to prevent creating default dashboards during test execution. + """ + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ) as mock_onboarding: + yield mock_onboarding + + async def test_lovelace_from_storage( hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] ) -> None: @@ -277,7 +291,7 @@ async def test_dashboard_from_yaml( async def test_wrong_key_dashboard_from_yaml(hass: HomeAssistant) -> None: """Test we don't load lovelace dashboard without hyphen config from yaml.""" - with assert_setup_component(0): + with assert_setup_component(0, "lovelace"): assert not await async_setup_component( hass, "lovelace", diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py new file mode 100644 index 00000000000000..a88745e450019b --- /dev/null +++ b/tests/components/lovelace/test_init.py @@ -0,0 +1,98 @@ +"""Test the Lovelace initialization.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.typing import WebSocketGenerator + + +@pytest.fixture +def mock_onboarding_not_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + yield mock_onboarding + + +@pytest.fixture +def mock_onboarding_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ) as mock_onboarding: + yield mock_onboarding + + +@pytest.fixture +def mock_add_onboarding_listener() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_add_listener", + ) as mock_add_onboarding_listener: + yield mock_add_onboarding_listener + + +async def test_create_dashboards_when_onboarded( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], + mock_onboarding_done, +) -> None: + """Test we don't create dashboards when onboarded.""" + client = await hass_ws_client(hass) + + assert await async_setup_component(hass, "lovelace", {}) + + # List dashboards + await client.send_json_auto_id({"type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + +async def test_create_dashboards_when_not_onboarded( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], + mock_add_onboarding_listener, + mock_onboarding_not_done, +) -> None: + """Test we automatically create dashboards when not onboarded.""" + client = await hass_ws_client(hass) + + assert await async_setup_component(hass, "lovelace", {}) + + # Call onboarding listener + mock_add_onboarding_listener.mock_calls[0][1][1]() + await hass.async_block_till_done() + + # List dashboards + await client.send_json_auto_id({"type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "icon": "mdi:map", + "id": "map", + "mode": "storage", + "require_admin": False, + "show_in_sidebar": True, + "title": "Map", + "url_path": "map", + } + ] + + # List map dashboard config + await client.send_json_auto_id({"type": "lovelace/config", "url_path": "map"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"strategy": {"type": "map"}} diff --git a/tests/components/lovelace/test_system_health.py b/tests/components/lovelace/test_system_health.py index 11cb8776e6262f..9bd8543004cce4 100644 --- a/tests/components/lovelace/test_system_health.py +++ b/tests/components/lovelace/test_system_health.py @@ -1,7 +1,10 @@ """Tests for Lovelace system health.""" +from collections.abc import Generator from typing import Any -from unittest.mock import patch +from unittest.mock import MagicMock, patch + +import pytest from homeassistant.components.lovelace import dashboard from homeassistant.core import HomeAssistant @@ -10,6 +13,19 @@ from tests.common import get_system_health_info +@pytest.fixture(autouse=True) +def mock_onboarding_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding. + + Enabled to prevent creating default dashboards during test execution. + """ + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ) as mock_onboarding: + yield mock_onboarding + + async def test_system_health_info_autogen(hass: HomeAssistant) -> None: """Test system health info endpoint.""" assert await async_setup_component(hass, "lovelace", {}) diff --git a/tests/components/map/__init__.py b/tests/components/map/__init__.py new file mode 100644 index 00000000000000..142afc0d5c985e --- /dev/null +++ b/tests/components/map/__init__.py @@ -0,0 +1 @@ +"""Tests for Map.""" diff --git a/tests/components/map/test_init.py b/tests/components/map/test_init.py new file mode 100644 index 00000000000000..6d79afefab3743 --- /dev/null +++ b/tests/components/map/test_init.py @@ -0,0 +1,116 @@ +"""Test the Map initialization.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.map import DOMAIN +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from tests.common import MockModule, mock_integration + + +@pytest.fixture +def mock_onboarding_not_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + yield mock_onboarding + + +@pytest.fixture +def mock_onboarding_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ) as mock_onboarding: + yield mock_onboarding + + +@pytest.fixture +def mock_create_map_dashboard() -> Generator[MagicMock, None, None]: + """Mock the create map dashboard function.""" + with patch( + "homeassistant.components.map._create_map_dashboard", + ) as mock_create_map_dashboard: + yield mock_create_map_dashboard + + +async def test_create_dashboards_when_onboarded( + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_onboarding_done, + mock_create_map_dashboard, +) -> None: + """Test we create map dashboard when onboarded.""" + # Mock the lovelace integration to prevent it from creating a map dashboard + mock_integration(hass, MockModule("lovelace")) + + assert await async_setup_component(hass, DOMAIN, {}) + + mock_create_map_dashboard.assert_called_once() + assert hass_storage[DOMAIN]["data"] == {"migrated": True} + + +async def test_create_dashboards_once_when_onboarded( + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_onboarding_done, + mock_create_map_dashboard, +) -> None: + """Test we create map dashboard once when onboarded.""" + hass_storage[DOMAIN] = { + "version": 1, + "minor_version": 1, + "key": "map", + "data": {"migrated": True}, + } + + # Mock the lovelace integration to prevent it from creating a map dashboard + mock_integration(hass, MockModule("lovelace")) + + assert await async_setup_component(hass, DOMAIN, {}) + + mock_create_map_dashboard.assert_not_called() + assert hass_storage[DOMAIN]["data"] == {"migrated": True} + + +async def test_create_dashboards_when_not_onboarded( + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_onboarding_not_done, + mock_create_map_dashboard, +) -> None: + """Test we do not create map dashboard when not onboarded.""" + # Mock the lovelace integration to prevent it from creating a map dashboard + mock_integration(hass, MockModule("lovelace")) + + assert await async_setup_component(hass, DOMAIN, {}) + + mock_create_map_dashboard.assert_not_called() + assert hass_storage[DOMAIN]["data"] == {"migrated": True} + + +async def test_create_issue_when_not_manually_configured(hass: HomeAssistant) -> None: + """Test creating issue registry issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + issue_registry = ir.async_get(hass) + assert not issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_yaml_map" + ) + + +async def test_create_issue_when_manually_configured(hass: HomeAssistant) -> None: + """Test creating issue registry issues.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + issue_registry = ir.async_get(hass) + assert issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, "deprecated_yaml_map") diff --git a/tests/components/onboarding/test_init.py b/tests/components/onboarding/test_init.py index 690630573877c0..f6941098b18292 100644 --- a/tests/components/onboarding/test_init.py +++ b/tests/components/onboarding/test_init.py @@ -48,10 +48,10 @@ async def test_is_onboarded() -> None: assert onboarding.async_is_onboarded(hass) - hass.data[onboarding.DOMAIN] = True + hass.data[onboarding.DOMAIN] = onboarding.OnboardingData([], True, {"done": []}) assert onboarding.async_is_onboarded(hass) - hass.data[onboarding.DOMAIN] = {"done": []} + hass.data[onboarding.DOMAIN] = onboarding.OnboardingData([], False, {"done": []}) assert not onboarding.async_is_onboarded(hass) @@ -62,10 +62,15 @@ async def test_is_user_onboarded() -> None: assert onboarding.async_is_user_onboarded(hass) - hass.data[onboarding.DOMAIN] = True + hass.data[onboarding.DOMAIN] = onboarding.OnboardingData([], True, {"done": []}) assert onboarding.async_is_user_onboarded(hass) - hass.data[onboarding.DOMAIN] = {"done": []} + hass.data[onboarding.DOMAIN] = onboarding.OnboardingData( + [], False, {"done": ["user"]} + ) + assert onboarding.async_is_user_onboarded(hass) + + hass.data[onboarding.DOMAIN] = onboarding.OnboardingData([], False, {"done": []}) assert not onboarding.async_is_user_onboarded(hass) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 406faff0dac6bb..272d0e997733fb 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -4,7 +4,7 @@ from http import HTTPStatus import os from typing import Any -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest @@ -655,3 +655,64 @@ async def test_onboarding_installation_type_after_done( resp = await client.get("/api/onboarding/installation_type") assert resp.status == 401 + + +async def test_complete_onboarding( + hass: HomeAssistant, hass_client: ClientSessionGenerator +) -> None: + """Test completing onboarding calls listeners.""" + listener_1 = Mock() + onboarding.async_add_listener(hass, listener_1) + listener_1.assert_not_called() + + assert await async_setup_component(hass, "onboarding", {}) + await hass.async_block_till_done() + + listener_2 = Mock() + onboarding.async_add_listener(hass, listener_2) + listener_2.assert_not_called() + + client = await hass_client() + + assert not onboarding.async_is_onboarded(hass) + + # Complete the user step + resp = await client.post( + "/api/onboarding/users", + json={ + "client_id": CLIENT_ID, + "name": "Test Name", + "username": "test-user", + "password": "test-pass", + "language": "en", + }, + ) + assert resp.status == 200 + assert not onboarding.async_is_onboarded(hass) + listener_2.assert_not_called() + + # Complete the core config step + resp = await client.post("/api/onboarding/core_config") + assert resp.status == 200 + assert not onboarding.async_is_onboarded(hass) + listener_2.assert_not_called() + + # Complete the integration step + resp = await client.post( + "/api/onboarding/integration", + json={"client_id": CLIENT_ID, "redirect_uri": CLIENT_REDIRECT_URI}, + ) + assert resp.status == 200 + assert not onboarding.async_is_onboarded(hass) + listener_2.assert_not_called() + + # Complete the analytics step + resp = await client.post("/api/onboarding/analytics") + assert resp.status == 200 + assert onboarding.async_is_onboarded(hass) + listener_1.assert_not_called() # Registered before the integration was setup + listener_2.assert_called_once_with() + + listener_3 = Mock() + onboarding.async_add_listener(hass, listener_3) + listener_3.assert_called_once_with()