diff --git a/homeassistant/components/picnic/services.py b/homeassistant/components/picnic/services.py index 3af2a521f8a..fa00037462d 100644 --- a/homeassistant/components/picnic/services.py +++ b/homeassistant/components/picnic/services.py @@ -66,7 +66,7 @@ async def handle_add_product( product_id = call.data.get("product_id") if not product_id: product_id = await hass.async_add_executor_job( - _product_search, api_client, cast(str, call.data["product_name"]) + product_search, api_client, cast(str, call.data["product_name"]) ) if not product_id: @@ -77,7 +77,7 @@ async def handle_add_product( ) -def _product_search(api_client: PicnicAPI, product_name: str) -> None | str: +def product_search(api_client: PicnicAPI, product_name: str) -> None | str: """Query the api client for the product name.""" search_result = api_client.search(product_name) diff --git a/homeassistant/components/picnic/todo.py b/homeassistant/components/picnic/todo.py index 8210702e826..389909ca06e 100644 --- a/homeassistant/components/picnic/todo.py +++ b/homeassistant/components/picnic/todo.py @@ -4,7 +4,12 @@ from __future__ import annotations import logging from typing import Any, cast -from homeassistant.components.todo import TodoItem, TodoItemStatus, TodoListEntity +from homeassistant.components.todo import ( + TodoItem, + TodoItemStatus, + TodoListEntity, + TodoListEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -15,6 +20,7 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import CONF_COORDINATOR, DOMAIN +from .services import product_search _LOGGER = logging.getLogger(__name__) @@ -27,19 +33,20 @@ async def async_setup_entry( """Set up the Picnic shopping cart todo platform config entry.""" picnic_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR] - # Add an entity shopping card - async_add_entities([PicnicCart(picnic_coordinator, config_entry)]) + async_add_entities([PicnicCart(hass, picnic_coordinator, config_entry)]) class PicnicCart(TodoListEntity, CoordinatorEntity): """A Picnic Shopping Cart TodoListEntity.""" _attr_has_entity_name = True - _attr_translation_key = "shopping_cart" _attr_icon = "mdi:cart" + _attr_supported_features = TodoListEntityFeature.CREATE_TODO_ITEM + _attr_translation_key = "shopping_cart" def __init__( self, + hass: HomeAssistant, coordinator: DataUpdateCoordinator[Any], config_entry: ConfigEntry, ) -> None: @@ -51,6 +58,7 @@ class PicnicCart(TodoListEntity, CoordinatorEntity): manufacturer="Picnic", model=config_entry.unique_id, ) + self.hass = hass self._attr_unique_id = f"{config_entry.unique_id}-cart" @property @@ -73,3 +81,18 @@ class PicnicCart(TodoListEntity, CoordinatorEntity): ) return items + + async def async_create_todo_item(self, item: TodoItem) -> None: + """Add item to shopping cart.""" + product_id = await self.hass.async_add_executor_job( + product_search, self.coordinator.picnic_api_client, item.summary + ) + + if not product_id: + raise ValueError("No product found or no product ID given") + + await self.hass.async_add_executor_job( + self.coordinator.picnic_api_client.add_product, product_id, 1 + ) + + await self.coordinator.async_refresh() diff --git a/tests/components/picnic/conftest.py b/tests/components/picnic/conftest.py index 7e36371767d..fb6c99f35e9 100644 --- a/tests/components/picnic/conftest.py +++ b/tests/components/picnic/conftest.py @@ -1,4 +1,5 @@ """Conftest for Picnic tests.""" +from collections.abc import Awaitable, Callable import json from unittest.mock import MagicMock, patch @@ -9,6 +10,9 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture +from tests.typing import WebSocketGenerator + +ENTITY_ID = "todo.mock_title_shopping_cart" @pytest.fixture @@ -50,3 +54,42 @@ async def init_integration( await hass.async_block_till_done() return mock_config_entry + + +@pytest.fixture +def ws_req_id() -> Callable[[], int]: + """Fixture for incremental websocket requests.""" + + id = 0 + + def next_id() -> int: + nonlocal id + id += 1 + return id + + return next_id + + +@pytest.fixture +async def get_items( + hass_ws_client: WebSocketGenerator, ws_req_id: Callable[[], int] +) -> Callable[[], Awaitable[dict[str, str]]]: + """Fixture to fetch items from the todo websocket.""" + + async def get() -> list[dict[str, str]]: + # Fetch items using To-do platform + client = await hass_ws_client() + id = ws_req_id() + await client.send_json( + { + "id": id, + "type": "todo/item/list", + "entity_id": ENTITY_ID, + } + ) + resp = await client.receive_json() + assert resp.get("id") == id + assert resp.get("success") + return resp.get("result", {}).get("items", []) + + return get diff --git a/tests/components/picnic/snapshots/test_todo.ambr b/tests/components/picnic/snapshots/test_todo.ambr new file mode 100644 index 00000000000..4b92584c0fc --- /dev/null +++ b/tests/components/picnic/snapshots/test_todo.ambr @@ -0,0 +1,55 @@ +# serializer version: 1 +# name: test_cart_list_with_items + list([ + dict({ + 'status': 'needs_action', + 'summary': 'Knoflook (2 stuks)', + 'uid': '763-s1001194', + }), + dict({ + 'status': 'needs_action', + 'summary': 'Picnic magere melk (2 x 1 liter)', + 'uid': '765_766-s1046297', + }), + dict({ + 'status': 'needs_action', + 'summary': 'Picnic magere melk (1 liter)', + 'uid': '767-s1010532', + }), + dict({ + 'status': 'needs_action', + 'summary': 'Robijn wascapsules wit (40 wasbeurten)', + 'uid': '774_775-s1018253', + }), + dict({ + 'status': 'needs_action', + 'summary': 'Robijn wascapsules kleur (15 wasbeurten)', + 'uid': '774_775-s1007025', + }), + dict({ + 'status': 'needs_action', + 'summary': 'Chinese wokgroenten (600 gram)', + 'uid': '776_777_778_779_780-s1012699', + }), + dict({ + 'status': 'needs_action', + 'summary': 'Picnic boerderij-eitjes (6 stuks M/L)', + 'uid': '776_777_778_779_780-s1003425', + }), + dict({ + 'status': 'needs_action', + 'summary': 'Picnic witte snelkookrijst (400 gram)', + 'uid': '776_777_778_779_780-s1016692', + }), + dict({ + 'status': 'needs_action', + 'summary': 'Conimex kruidenmix nasi (20 gram)', + 'uid': '776_777_778_779_780-s1012503', + }), + dict({ + 'status': 'needs_action', + 'summary': 'Conimex satésaus mild kant & klaar (400 gram)', + 'uid': '776_777_778_779_780-s1005028', + }), + ]) +# --- diff --git a/tests/components/picnic/test_todo.py b/tests/components/picnic/test_todo.py index 675651dc588..a65fb83ca95 100644 --- a/tests/components/picnic/test_todo.py +++ b/tests/components/picnic/test_todo.py @@ -1,18 +1,31 @@ """Tests for Picnic Tasks todo platform.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, Mock +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.todo import DOMAIN from homeassistant.core import HomeAssistant +from .conftest import ENTITY_ID + from tests.common import MockConfigEntry -async def test_cart_list_with_items(hass: HomeAssistant, init_integration) -> None: +async def test_cart_list_with_items( + hass: HomeAssistant, + init_integration, + get_items, + snapshot: SnapshotAssertion, +) -> None: """Test loading of shopping cart.""" - state = hass.states.get("todo.mock_title_shopping_cart") + state = hass.states.get(ENTITY_ID) assert state assert state.state == "10" + assert snapshot == await get_items() + async def test_cart_list_empty_items( hass: HomeAssistant, mock_picnic_api: MagicMock, mock_config_entry: MockConfigEntry @@ -23,7 +36,7 @@ async def test_cart_list_empty_items( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("todo.mock_title_shopping_cart") + state = hass.states.get(ENTITY_ID) assert state assert state.state == "0" @@ -37,7 +50,7 @@ async def test_cart_list_unexpected_response( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("todo.mock_title_shopping_cart") + state = hass.states.get(ENTITY_ID) assert state is None @@ -50,5 +63,63 @@ async def test_cart_list_null_response( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("todo.mock_title_shopping_cart") + state = hass.states.get(ENTITY_ID) assert state is None + + +async def test_create_todo_list_item( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_picnic_api: MagicMock +) -> None: + """Test for creating a picnic cart item.""" + assert len(mock_picnic_api.get_cart.mock_calls) == 1 + + mock_picnic_api.search = Mock() + mock_picnic_api.search.return_value = [ + { + "items": [ + { + "id": 321, + "name": "Picnic Melk", + "unit_quantity": "2 liter", + } + ] + } + ] + + mock_picnic_api.add_product = Mock() + + await hass.services.async_call( + DOMAIN, + "add_item", + {"item": "Melk"}, + target={"entity_id": ENTITY_ID}, + blocking=True, + ) + + args = mock_picnic_api.search.call_args + assert args + assert args[0][0] == "Melk" + + args = mock_picnic_api.add_product.call_args + assert args + assert args[0][0] == "321" + assert args[0][1] == 1 + + assert len(mock_picnic_api.get_cart.mock_calls) == 2 + + +async def test_create_todo_list_item_not_found( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_picnic_api: MagicMock +) -> None: + """Test for creating a picnic cart item when ID is not found.""" + mock_picnic_api.search = Mock() + mock_picnic_api.search.return_value = [{"items": []}] + + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + "add_item", + {"item": "Melk"}, + target={"entity_id": ENTITY_ID}, + blocking=True, + )