Files
localthings/tests/test_select_options.py
Marc Billow 10c850f2b9 refactor(i18n): drop the vestigial descriptor name field
Since entity.py started routing named descriptors through the catalog
under desc.key, SamsungEntityDescription.name has been read for its
value nowhere -- only twice as a flag, to decide whether an entity was
translated at all. That left 148 English names duplicated between Python
and translations/en.json with nothing keeping them honest: six had
already drifted, invisibly, because editing the Python side changes
nothing a user sees.

So the field is gone, and translation_key defaults to desc.key. A
descriptor now sets translation_key only to share one catalog entry
across descriptors or to point at a differently-named one, and the
catalog is the only place an entity name exists.

Every descriptor resolves to exactly the translation key, icon, entity
category, enabled-default and gating it did before -- with one
deliberate exception: the hood fan, previously the sole descriptor with
no key at all, now resolves to 'fan'. That is inert, because fan.py sets
_attr_name = None so the entity presents as the device itself.

The three helpers that forwarded a name into a descriptor
(laundry.bool_option_switch, washer._bool_option_switch, air_purifier's
sensor table) lose that parameter. test_translations.py now requires a
catalog entry for every descriptor rather than only translated ones.

Claude-Session: https://claude.ai/code/session_01GiibJZZLWVvyxq7mc7EDNp
2026-07-24 19:45:37 +00:00

103 lines
4.0 KiB
Python

"""Tests for LocalThingsSelect's option-list resolution
(custom_components/localthings/select.py) -- the static tuple, options_field,
and callable forms of SelectDesc.options.
"""
from custom_components.localthings.registry.capability import Capability
from custom_components.localthings.registry.discovery import BoundEntity
from custom_components.localthings.registry.entities import SelectDesc
from custom_components.localthings.select import LocalThingsSelect
class _FakeCoordinator:
device_serial = 'TEST-SERIAL'
def __init__(self, last_resources):
self.last_resources = last_resources
def _make_select(desc, href, last_resources):
capability = Capability(href=href, entities=(desc,))
bound = BoundEntity(href=href, capability=capability, desc=desc)
return LocalThingsSelect(_FakeCoordinator(last_resources), bound)
def test_static_options_unaffected():
desc = SelectDesc(key='x', options=('A', 'B'))
entity = _make_select(desc, '/x/vs/0', {})
assert entity.options == ['A', 'B']
def test_options_field_unaffected():
desc = SelectDesc(key='x', options_field='supported')
entity = _make_select(desc, '/x/vs/0', {'/x/vs/0': {'supported': ['Lo', 'Hi']}})
assert entity.options == ['Lo', 'Hi']
def test_callable_options_receives_full_resource_snapshot():
"""A callable options is handed the coordinator's full href->rep
snapshot, not just this entity's own href's rep -- needed for course
lists decoded from a sibling resource (see laundry.cycle_options)."""
calls = []
def _options_fn(resources):
calls.append(resources)
return list(resources.get('/other/vs/0', {}).get('codes', []))
desc = SelectDesc(key='cycle', translation_key='fake_cycle', options=_options_fn)
resources = {
'/x/vs/0': {},
'/other/vs/0': {'codes': ['1C', '1D']},
}
entity = _make_select(desc, '/x/vs/0', resources)
assert entity.options == ['1C', '1D']
assert calls == [resources]
def test_callable_options_empty_result():
desc = SelectDesc(key='cycle', options=lambda resources: [])
entity = _make_select(desc, '/x/vs/0', {})
assert entity.options == []
def test_callable_translation_key_reresolves_live_not_once_at_construction():
"""A callable translation_key (laundry.cycle_select's table-id-gated
resolver) must be re-evaluated against current coordinator data on
every access, not baked in once at __init__ -- discovery can run while
a sibling resource (e.g. /st/washercourse/vs/0) is still an empty stub
(see entity.py's _is_included docstring), and a one-time resolution
would permanently show untranslated codes even after a later poll
populates the real value."""
desc = SelectDesc(key='cycle', translation_key=lambda resources: resources.get('key'))
resources = {'key': None}
entity = _make_select(desc, '/x/vs/0', resources)
assert entity.translation_key is None
resources['key'] = 'washer_cycle_table_02'
assert entity.translation_key == 'washer_cycle_table_02'
async def test_unknown_vendor_option_round_trips_to_exact_raw_value():
"""Readable fallback labels must still write the exact Samsung token."""
class _WritableCoordinator(_FakeCoordinator):
data = {'mode': 'FutureVendorMode'}
def __init__(self, last_resources):
super().__init__(last_resources)
self.writes = []
async def async_send_command(self, bound, value):
self.writes.append(value)
desc = SelectDesc(
key='mode', translation_key='door_alert',
options=('Known', 'FutureVendorMode'), write_fn=lambda *args: None,
)
capability = Capability(href='/x/vs/0', entities=(desc,))
bound = BoundEntity(href='/x/vs/0', capability=capability, desc=desc)
coordinator = _WritableCoordinator({})
entity = LocalThingsSelect(coordinator, bound)
assert entity.options[-1] == 'Future Vendor Mode'
await entity.async_select_option('Future Vendor Mode')
assert coordinator.writes == ['FutureVendorMode']