what_to_wear/tests/test_subentry_flow.py
Nora e877efa265 feat(2.1): Kleidungsstücke verwalten (ItemSubentryFlow)
Story 2.1 (TDD, phcc, Suite gruen 114/114):
- config_flow.py: async_get_supported_subentry_types + ItemSubentryFlow (user +
  reconfigure mit Kompat-Accessor 2025.3/2025.4), Item-Schema (AD-22), Validierung
  (name 1-60, temp_range), Werte-Erhalt bei Fehler.
- coordinator/__init__: Options-vs-Subentry-Unterscheidung via Snapshot (Options->reload,
  Subentry->refresh, AD-7).
- translations/{en,de}.json: config_subentries + selector-Optionen (AD-13).
- luna-pro-Story-Review: 3/4 Findings uebernommen, 1 verworfen (Evidenz im Ledger).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:29:52 +00:00

139 lines
6.1 KiB
Python

"""Story 2.1 — wardrobe item subentry flow (add/edit) and recompute."""
from __future__ import annotations
from datetime import timedelta
from homeassistant.components.weather import WeatherEntityFeature
from homeassistant.config_entries import ConfigSubentry, ConfigSubentryData
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.util import dt as dt_util
from pytest_homeassistant_custom_component.common import MockConfigEntry
from types import MappingProxyType
from custom_components.what_to_wear.config_flow import (
ItemSubentryFlow,
_clean_item,
_validate_item,
)
from custom_components.what_to_wear.const import CONF_WEATHER_ENTITY, DOMAIN, SENSOR_ENTITY_ID
from custom_components.what_to_wear.coordinator import build_items
ENTITY = "weather.home"
def _set_weather(hass) -> None:
hass.states.async_set(
ENTITY, "cloudy",
{"temperature_unit": "°C", "wind_speed_unit": "km/h", "precipitation_unit": "mm",
"supported_features": WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY},
)
def _register_forecast(hass) -> None:
async def handler(call) -> ServiceResponse:
base = dt_util.now()
entries = [{"datetime": (base + timedelta(days=o)).replace(hour=12), "temperature": 8.0,
"templow": 5.0} for o in range(-1, 4)]
return {call.data["entity_id"]: {"forecast": entries}}
hass.services.async_register("weather", "get_forecasts", handler,
supports_response=SupportsResponse.ONLY)
async def _setup(hass, subentries=()):
_set_weather(hass)
_register_forecast(hass)
entry = MockConfigEntry(domain=DOMAIN, data={CONF_WEATHER_ENTITY: ENTITY}, options={},
subentries_data=list(subentries))
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
def test_validate_and_clean() -> None:
assert _validate_item({"name": "", "category": "top", "warmth": 2}) == "invalid_name"
assert _validate_item({"name": "X" * 61, "category": "top", "warmth": 2}) == "invalid_name"
assert _validate_item({"name": "A", "category": "top", "warmth": 2,
"temp_min": 20, "temp_max": 10}) == "temp_range"
assert _validate_item({"name": "Jeans", "category": "bottom", "warmth": 3}) is None
cleaned = _clean_item({"name": " Jeans ", "category": "bottom", "warmth": "3",
"waterproof": True})
assert cleaned == {"name": "Jeans", "category": "bottom", "warmth": 3, "waterproof": True,
"windproof": False, "sun_protection": False, "formality": "casual",
"active": True}
async def test_add_item_via_subentry_flow(hass: HomeAssistant) -> None:
entry = await _setup(hass)
result = await hass.config_entries.subentries.async_init(
(entry.entry_id, "item"), context={"source": "user"}
)
assert result["type"] == FlowResultType.FORM
result2 = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{"name": "Jeans", "category": "bottom", "warmth": 3},
)
assert result2["type"] == FlowResultType.CREATE_ENTRY
await hass.async_block_till_done()
# the item is now stored as a subentry and flows into the recommendation
items = build_items(entry)
assert any(i.name == "Jeans" for i in items)
async def test_add_item_invalid_name_shows_error(hass: HomeAssistant) -> None:
entry = await _setup(hass)
result = await hass.config_entries.subentries.async_init(
(entry.entry_id, "item"), context={"source": "user"}
)
result2 = await hass.config_entries.subentries.async_configure(
result["flow_id"], {"name": "", "category": "top", "warmth": 2}
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "invalid_name"}
async def test_supported_subentry_types(hass: HomeAssistant) -> None:
from custom_components.what_to_wear.config_flow import WhatToWearConfigFlow
entry = MockConfigEntry(domain=DOMAIN, data={CONF_WEATHER_ENTITY: ENTITY})
types = WhatToWearConfigFlow.async_get_supported_subentry_types(entry)
assert types == {"item": ItemSubentryFlow}
async def test_subentry_change_triggers_recompute(hass: HomeAssistant) -> None:
entry = await _setup(hass)
for name, cat in [("Shirt", "top"), ("Jeans", "bottom"), ("Boots", "shoes")]:
sub = ConfigSubentry(
data=MappingProxyType({"name": name, "category": cat, "warmth": 3}),
subentry_type="item", title=name, unique_id=None,
)
assert hass.config_entries.async_add_subentry(entry, sub)
# A subentry change routes to a refresh (not a reload); force the pending
# recompute deterministically and verify the items flow into the sensor.
coordinator = hass.data[DOMAIN][entry.entry_id]
await coordinator.async_refresh()
await hass.async_block_till_done()
state = hass.states.get(SENSOR_ENTITY_ID)
names = {it["name"] for it in state.attributes["items"]}
assert {"Shirt", "Jeans", "Boots"} <= names
async def test_single_subentry_add_auto_refreshes(hass: HomeAssistant) -> None:
# A single item add fires the update listener -> subentry route -> refresh.
entry = await _setup(hass, [
ConfigSubentryData(data={"name": "Jeans", "category": "bottom", "warmth": 3},
subentry_type="item", title="Jeans", unique_id=None),
ConfigSubentryData(data={"name": "Boots", "category": "shoes", "warmth": 3},
subentry_type="item", title="Boots", unique_id=None),
])
sub = ConfigSubentry(
data=MappingProxyType({"name": "Shirt", "category": "top", "warmth": 3}),
subentry_type="item", title="Shirt", unique_id=None,
)
assert hass.config_entries.async_add_subentry(entry, sub)
await hass.async_block_till_done()
state = hass.states.get(SENSOR_ENTITY_ID)
names = {it["name"] for it in state.attributes["items"]}
assert "Shirt" in names # the newly added item was picked up automatically