Auto-register the zero-config panel dashboard on setup

A fresh HA install with children configured should show the panel
with no YAML and no manual dashboard creation: setup now registers
ONE storage-mode Lovelace dashboard whose config is the bundle's
custom:nestquest-party strategy (frontend/src/strategy.ts), under the
stable url_path 'nestquest' with a title, icon and sidebar visibility.
Registration is idempotent — the url_path is the idempotency key, so
a reload or a second entry never duplicates the dashboard, and a
dashboard already occupying the url_path (ours or the user's) is left
completely alone (never overwritten, never deleted).  An unavailable
Lovelace dashboards collection (YAML mode, lovelace not loaded) or a
refused create logs a clear warning and setup continues — never a
crash.  Tests cover create-on-setup, no-duplicate-on-reload, the
untouched user dashboard, the missing collection and a refusing one.
This commit is contained in:
2026-09-23 10:17:20 -04:00
parent 6d570e1c0e
commit f668cf95b0
4 changed files with 245 additions and 1 deletions
+110
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from types import SimpleNamespace
import pytest
@@ -12,12 +13,42 @@ from custom_components.nestquest.const import (
DOMAIN as DOMAIN_CONST,
SERVICE_COMPLETE_QUEST,
)
from custom_components.nestquest.dashboard import (
DASHBOARD_STRATEGY_TYPE,
DASHBOARD_URL_PATH,
)
def _wire(entry, registry):
return wire_entry_to_registry(entry, registry)
class FakeDashboardsCollection:
"""Stand-in for the Lovelace dashboards collection.
Records ``async_create_item`` calls the way the real collection
would persist them, so tests can assert exactly what the setup path
registered — and that a reload registers nothing more.
"""
def __init__(self, items=None):
self.items = list(items or [])
self.created: list[dict] = []
async def async_items(self):
return list(self.items)
async def async_create_item(self, item_config: dict) -> None:
self.created.append(dict(item_config))
self.items.append(dict(item_config))
def _install_dashboard_collection(hass, items=None) -> FakeDashboardsCollection:
collection = FakeDashboardsCollection(items)
hass.data["lovelace"] = SimpleNamespace(dashboards=collection)
return collection
async def test_setup_stores_runtime_data_in_hass_data(hass, make_entry) -> None:
entry = _wire(make_entry(), hass.registry)
assert await async_setup_entry(hass, entry) is True
@@ -271,3 +302,82 @@ async def test_unload_shuts_coordinator_even_when_listener_removal_raises() -> N
assert entry.runtime_data is None
assert DOMAIN_CONST not in hass.data
assert hass.time_change.size == 0
async def test_setup_creates_the_panel_dashboard(hass, make_entry) -> None:
"""A fresh install gets the storage-mode strategy dashboard, with the
stable url_path, a title/icon, sidebar visibility and no YAML."""
collection = _install_dashboard_collection(hass)
entry = _wire(make_entry(), hass.registry)
assert await async_setup_entry(hass, entry) is True
assert len(collection.created) == 1
created = collection.created[0]
assert created["url_path"] == DASHBOARD_URL_PATH
assert created["mode"] == "storage"
assert created["strategy"] == {"type": DASHBOARD_STRATEGY_TYPE}
assert created["title"]
assert created["show_in_sidebar"] is True
assert "icon" in created
async def test_setup_reload_does_not_duplicate_the_dashboard(
hass, make_entry
) -> None:
"""A reload (or a second entry) must not create a second dashboard:
the one under the stable url_path is reused as-is."""
collection = _install_dashboard_collection(hass)
entry = _wire(make_entry(), hass.registry)
assert await async_setup_entry(hass, entry) is True
assert len(collection.created) == 1
# A reload: setup again on the same hass (the reload cycle) —
# the existing dashboard must be left alone.
await async_unload_entry(hass, entry)
assert await async_setup_entry(hass, entry) is True
assert len(collection.created) == 1
# And a second entry on the same install.
other = _wire(make_entry(entry_id="other"), hass.registry)
assert await async_setup_entry(hass, other) is True
assert len(collection.created) == 1
async def test_setup_leaves_an_existing_user_dashboard_alone(
hass, make_entry
) -> None:
"""A dashboard already occupying the stable url_path — the user's
own — is never overwritten, deleted or duplicated."""
users_dashboard = {"url_path": DASHBOARD_URL_PATH, "title": "My Board"}
collection = _install_dashboard_collection(hass, items=[users_dashboard])
entry = _wire(make_entry(), hass.registry)
assert await async_setup_entry(hass, entry) is True
assert collection.created == []
assert collection.items[0] is users_dashboard
async def test_setup_succeeds_without_the_dashboards_collection(
hass, make_entry, caplog
) -> None:
"""YAML mode / lovelace not loaded: log a warning, continue setup."""
entry = _wire(make_entry(), hass.registry)
assert await async_setup_entry(hass, entry) is True
assert entry.entry_id in hass.data[DOMAIN]
assert any(
"dashboard" in record.message.lower()
and record.levelname == "WARNING"
for record in caplog.records
)
async def test_setup_survives_a_failing_dashboards_collection(
hass, make_entry
) -> None:
"""A collection that refuses the create (permissions, admin-only)
logs a warning and setup still succeeds — entities come up."""
class _RefusingCollection(FakeDashboardsCollection):
async def async_create_item(self, item_config):
raise PermissionError("not an admin")
collection = _RefusingCollection()
hass.data["lovelace"] = SimpleNamespace(dashboards=collection)
entry = _wire(make_entry(), hass.registry)
assert await async_setup_entry(hass, entry) is True
assert collection.created == []