Test translation coverage and dynamic fallbacks
This commit is contained in:
@@ -38,6 +38,7 @@ async def test_form_second_device_reuses_creds(hass: HomeAssistant) -> None:
|
|||||||
DOMAIN, context={'source': 'user'}
|
DOMAIN, context={'source': 'user'}
|
||||||
)
|
)
|
||||||
assert result['type'] == FlowResultType.FORM
|
assert result['type'] == FlowResultType.FORM
|
||||||
|
assert result['step_id'] == 'user_reuse'
|
||||||
assert CONF_CA_CERT_PEM not in result['data_schema'].schema
|
assert CONF_CA_CERT_PEM not in result['data_schema'].schema
|
||||||
assert CONF_CA_KEY_PEM not in result['data_schema'].schema
|
assert CONF_CA_KEY_PEM not in result['data_schema'].schema
|
||||||
|
|
||||||
@@ -219,6 +220,39 @@ async def test_unknown_type_shows_confirmation_step(
|
|||||||
assert result['data'][CONF_HOST] == MOCK_HOST
|
assert result['data'][CONF_HOST] == MOCK_HOST
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unknown_type_without_version_uses_localized_step(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""No English placeholder sentinel leaks into a translated description."""
|
||||||
|
probe_result = {
|
||||||
|
'port': MOCK_PORT,
|
||||||
|
'serial': MOCK_SERIAL,
|
||||||
|
'leaf_cert_pem': 'leaf cert',
|
||||||
|
'leaf_key_pem': 'leaf key',
|
||||||
|
'one_ui_version': '',
|
||||||
|
'device_type_recognized': False,
|
||||||
|
}
|
||||||
|
with patch(
|
||||||
|
'custom_components.localthings.config_flow._probe_and_validate',
|
||||||
|
return_value=probe_result,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={'source': 'user'}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result['flow_id'],
|
||||||
|
{
|
||||||
|
CONF_HOST: MOCK_HOST,
|
||||||
|
CONF_CA_CERT_PEM: MOCK_CA_CERT_PEM,
|
||||||
|
CONF_CA_KEY_PEM: MOCK_CA_KEY_PEM,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result['type'] == FlowResultType.FORM
|
||||||
|
assert result['step_id'] == 'confirm_unknown_type_no_version'
|
||||||
|
assert not result.get('description_placeholders')
|
||||||
|
|
||||||
|
|
||||||
async def test_duplicate_device_aborted(hass: HomeAssistant, mock_probe) -> None:
|
async def test_duplicate_device_aborted(hass: HomeAssistant, mock_probe) -> None:
|
||||||
"""Second add of same serial: flow aborts.
|
"""Second add of same serial: flow aborts.
|
||||||
|
|
||||||
|
|||||||
@@ -914,9 +914,12 @@ async def test_send_command_blocked_when_remote_control_disabled(
|
|||||||
|
|
||||||
with patch.object(fake, 'subscribe'):
|
with patch.object(fake, 'subscribe'):
|
||||||
fake.post = _post
|
fake.post = _post
|
||||||
with pytest.raises(ServiceValidationError, match="Remote control is turned off"):
|
with pytest.raises(ServiceValidationError) as exc_info:
|
||||||
await coordinator.async_send_command(bound, 5)
|
await coordinator.async_send_command(bound, 5)
|
||||||
|
|
||||||
|
assert exc_info.value.translation_domain == DOMAIN
|
||||||
|
assert exc_info.value.translation_key == 'remote_control_disabled'
|
||||||
|
|
||||||
assert posted is False
|
assert posted is False
|
||||||
|
|
||||||
|
|
||||||
@@ -977,8 +980,9 @@ async def test_send_command_remote_control_check_precedes_validate_fn(
|
|||||||
desc = SwitchDesc(key='test', field='value', write_fn=_write_fn, validate_fn=_validate_fn)
|
desc = SwitchDesc(key='test', field='value', write_fn=_write_fn, validate_fn=_validate_fn)
|
||||||
bound = BoundEntity(href='/test/vs/0', capability=coordinator.bound[0].capability, desc=desc)
|
bound = BoundEntity(href='/test/vs/0', capability=coordinator.bound[0].capability, desc=desc)
|
||||||
|
|
||||||
with pytest.raises(ServiceValidationError, match="Remote control is turned off"):
|
with pytest.raises(ServiceValidationError) as exc_info:
|
||||||
await coordinator.async_send_command(bound, 'On')
|
await coordinator.async_send_command(bound, 'On')
|
||||||
|
assert exc_info.value.translation_key == 'remote_control_disabled'
|
||||||
|
|
||||||
|
|
||||||
async def test_send_command_bypasses_remote_control_when_option_enabled(
|
async def test_send_command_bypasses_remote_control_when_option_enabled(
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ def test_course_bound_to_shared_course_vs_0():
|
|||||||
assert callable(desc.translation_key)
|
assert callable(desc.translation_key)
|
||||||
table_03 = {'/st/dryercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_03'}}
|
table_03 = {'/st/dryercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_03'}}
|
||||||
assert desc.translation_key(table_03) == 'dryer_cycle_table_03'
|
assert desc.translation_key(table_03) == 'dryer_cycle_table_03'
|
||||||
assert desc.translation_key({}) is None
|
assert desc.translation_key({}) == 'cycle'
|
||||||
assert desc.options is laundry.cycle_options
|
assert desc.options is laundry.cycle_options
|
||||||
rep = {'x.com.samsung.da.options': ['Course_16', 'GMT_02']}
|
rep = {'x.com.samsung.da.options': ['Course_16', 'GMT_02']}
|
||||||
assert desc.rep_fn(rep) == '16'
|
assert desc.rep_fn(rep) == '16'
|
||||||
|
|||||||
+25
-16
@@ -1,7 +1,4 @@
|
|||||||
"""Tests for LocalThingsEntity's display-name derivation
|
"""Tests for translated entity naming and dynamic instance placeholders."""
|
||||||
(custom_components/localthings/entity.py) -- the explicit-name,
|
|
||||||
device-given-instance-name, and href-derived fallbacks.
|
|
||||||
"""
|
|
||||||
from custom_components.localthings.entity import LocalThingsEntity
|
from custom_components.localthings.entity import LocalThingsEntity
|
||||||
from custom_components.localthings.registry.capability import Capability
|
from custom_components.localthings.registry.capability import Capability
|
||||||
from custom_components.localthings.registry.discovery import BoundEntity
|
from custom_components.localthings.registry.discovery import BoundEntity
|
||||||
@@ -23,23 +20,35 @@ def _make_entity(desc, href='/x/vs/0', key_override=None, instance='', instance_
|
|||||||
return LocalThingsEntity(_FakeCoordinator(), bound)
|
return LocalThingsEntity(_FakeCoordinator(), bound)
|
||||||
|
|
||||||
|
|
||||||
def test_explicit_name_wins_over_everything():
|
def test_explicit_fallback_name_uses_translation_instead_of_attr_name():
|
||||||
desc = BinarySensorDesc(key='enabled', name='Explicit Name')
|
"""An _attr_name would take precedence over HA's translation catalog."""
|
||||||
|
desc = BinarySensorDesc(key='enabled', name='Explicit name')
|
||||||
entity = _make_entity(desc, instance_name='Cubed Ice')
|
entity = _make_entity(desc, instance_name='Cubed Ice')
|
||||||
assert entity._attr_name == 'Explicit Name'
|
assert entity.translation_key == 'enabled'
|
||||||
|
assert not hasattr(entity, '_attr_name')
|
||||||
|
|
||||||
|
|
||||||
def test_instance_name_prefixes_the_derived_suffix():
|
def test_device_instance_name_becomes_translation_placeholder():
|
||||||
"""Issue #27: an ice maker's device-given name ("Cubed Ice") replaces
|
desc = BinarySensorDesc(
|
||||||
the href-derived instance label ("Icemaker One") as the name prefix,
|
key='enabled', translation_key='instance_enabled', use_instance_name=True
|
||||||
keeping the same entity-specific suffix."""
|
)
|
||||||
desc = BinarySensorDesc(key='enabled')
|
|
||||||
entity = _make_entity(desc, key_override='icemaker_one_enabled',
|
entity = _make_entity(desc, key_override='icemaker_one_enabled',
|
||||||
instance_name='Cubed Ice')
|
instance_name='Cubed Ice')
|
||||||
assert entity._attr_name == 'Cubed Ice Enabled'
|
assert entity.translation_key == 'instance_enabled'
|
||||||
|
assert entity.translation_placeholders == {'instance_name': 'Cubed Ice'}
|
||||||
|
assert not hasattr(entity, '_attr_name')
|
||||||
|
|
||||||
|
|
||||||
def test_no_instance_name_falls_back_to_derived_state_key():
|
def test_href_instance_name_becomes_translation_placeholder():
|
||||||
desc = BinarySensorDesc(key='enabled')
|
desc = BinarySensorDesc(
|
||||||
|
key='enabled', translation_key='instance_enabled', use_instance_name=True
|
||||||
|
)
|
||||||
entity = _make_entity(desc, key_override='icemaker_one_enabled')
|
entity = _make_entity(desc, key_override='icemaker_one_enabled')
|
||||||
assert entity._attr_name == 'Icemaker One Enabled'
|
assert entity.translation_placeholders == {'instance_name': 'Icemaker One'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_untranslated_vendor_entity_keeps_readable_fallback_name():
|
||||||
|
desc = BinarySensorDesc(key='vendor_feature')
|
||||||
|
entity = _make_entity(desc, key_override='vendor_feature_1')
|
||||||
|
assert entity.translation_key is None
|
||||||
|
assert entity._attr_name == 'Vendor Feature 1'
|
||||||
|
|||||||
@@ -173,11 +173,9 @@ class TestCycleSelectTableGating:
|
|||||||
codes aren't guaranteed consistent across board generations sharing
|
codes aren't guaranteed consistent across board generations sharing
|
||||||
the same /course/vs/0 contract; FlexWash's older board reports a
|
the same /course/vs/0 contract; FlexWash's older board reports a
|
||||||
different course table than every device the shipped translations
|
different course table than every device the shipped translations
|
||||||
were confirmed against). The resolved key is built from whatever table
|
were confirmed against). The resolved key is built from a reported
|
||||||
the device actually reports -- a table with no strings.json entries
|
table id. Devices without one use the generic translated Cycle entity
|
||||||
yet just falls through Home Assistant's own missing-translation
|
name while preserving raw option codes as a safe fallback."""
|
||||||
handling to raw-code display, the same as any individual untranslated
|
|
||||||
code within an existing table."""
|
|
||||||
|
|
||||||
def _desc(self):
|
def _desc(self):
|
||||||
return laundry.cycle_select(
|
return laundry.cycle_select(
|
||||||
@@ -198,20 +196,17 @@ class TestCycleSelectTableGating:
|
|||||||
assert callable(desc.translation_key)
|
assert callable(desc.translation_key)
|
||||||
assert desc.translation_key(resources) == 'washer_cycle_table_02'
|
assert desc.translation_key(resources) == 'washer_cycle_table_02'
|
||||||
|
|
||||||
def test_resolved_key_reflects_an_unbuilt_table_too(self):
|
def test_untranslated_table_uses_generic_cycle_key(self):
|
||||||
"""No gating against a hardcoded 'known good' table -- a table we
|
"""An unknown table does not claim another board's state labels."""
|
||||||
haven't shipped translations for yet still gets a key built for
|
|
||||||
it, just one strings.json has nothing under (raw-code display)."""
|
|
||||||
desc = self._desc()
|
desc = self._desc()
|
||||||
resources = {'/st/washercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_00'}}
|
resources = {'/st/washercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_00'}}
|
||||||
assert desc.translation_key(resources) == 'washer_cycle_table_00'
|
assert desc.translation_key(resources) == 'cycle'
|
||||||
|
|
||||||
def test_resolves_to_none_when_table_id_is_unknown(self):
|
def test_resolves_to_generic_cycle_when_table_id_is_unknown(self):
|
||||||
"""No href, or an empty rep, gets no translation_key at all --
|
"""An absent table id still gets a translated generic entity name."""
|
||||||
there's nothing to build a key from."""
|
|
||||||
desc = self._desc()
|
desc = self._desc()
|
||||||
assert desc.translation_key({}) is None
|
assert desc.translation_key({}) == 'cycle'
|
||||||
assert desc.translation_key({'/st/washercourse/vs/0': {}}) is None
|
assert desc.translation_key({'/st/washercourse/vs/0': {}}) == 'cycle'
|
||||||
|
|
||||||
|
|
||||||
class TestBuzzerSound:
|
class TestBuzzerSound:
|
||||||
|
|||||||
@@ -34,5 +34,14 @@ def test_display_lowercases_for_translation_key_lookup():
|
|||||||
assert _display('Whiskey_IceBall_3', _TRANSLATED) == 'whiskey_iceball_3'
|
assert _display('Whiskey_IceBall_3', _TRANSLATED) == 'whiskey_iceball_3'
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_translated_vendor_value_keeps_readable_fallback():
|
||||||
|
"""A firmware-added value must remain readable instead of being mangled."""
|
||||||
|
assert _display('FutureVendorMode', _TRANSLATED) == 'Future Vendor Mode'
|
||||||
|
|
||||||
|
|
||||||
|
def test_known_camel_case_state_uses_snake_case_translation_key():
|
||||||
|
assert _display('ExtraHigh', 'heated_dry') == 'extra_high'
|
||||||
|
|
||||||
|
|
||||||
def test_display_passes_through_non_string_values():
|
def test_display_passes_through_non_string_values():
|
||||||
assert _display(None, _UNTRANSLATED) is None
|
assert _display(None, _UNTRANSLATED) is None
|
||||||
|
|||||||
@@ -74,3 +74,29 @@ def test_callable_translation_key_reresolves_live_not_once_at_construction():
|
|||||||
|
|
||||||
resources['key'] = 'washer_cycle_table_02'
|
resources['key'] = 'washer_cycle_table_02'
|
||||||
assert entity.translation_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', name='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']
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""Translation architecture and catalog synchronization tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from string import Formatter
|
||||||
|
|
||||||
|
from custom_components.localthings.registry.capability import Capability
|
||||||
|
from custom_components.localthings.registry.entities import PLATFORM_OF
|
||||||
|
from custom_components.localthings.select import TRANSLATED_SELECT_STATES
|
||||||
|
|
||||||
|
|
||||||
|
INTEGRATION = (
|
||||||
|
Path(__file__).parents[1] / "custom_components" / "localthings"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load(name: str) -> dict:
|
||||||
|
return json.loads((INTEGRATION / name).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def _topology(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {key: _topology(child) for key, child in value.items()}
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [_topology(child) for child in value]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _placeholders(value: str) -> set[str]:
|
||||||
|
return {
|
||||||
|
field_name
|
||||||
|
for _, field_name, _, _ in Formatter().parse(value)
|
||||||
|
if field_name is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _walk_strings(value, path=()):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for key, child in value.items():
|
||||||
|
yield from _walk_strings(child, (*path, key))
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for index, child in enumerate(value):
|
||||||
|
yield from _walk_strings(child, (*path, str(index)))
|
||||||
|
elif isinstance(value, str):
|
||||||
|
yield path, value
|
||||||
|
|
||||||
|
|
||||||
|
def _all_descriptions():
|
||||||
|
capabilities_dir = INTEGRATION / "registry" / "capabilities"
|
||||||
|
seen: set[int] = set()
|
||||||
|
for module_path in capabilities_dir.glob("*.py"):
|
||||||
|
if module_path.stem == "__init__":
|
||||||
|
continue
|
||||||
|
module = importlib.import_module(
|
||||||
|
f"custom_components.localthings.registry.capabilities.{module_path.stem}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def visit(value):
|
||||||
|
if isinstance(value, Capability):
|
||||||
|
if id(value) in seen:
|
||||||
|
return
|
||||||
|
seen.add(id(value))
|
||||||
|
yield from value.entities
|
||||||
|
elif isinstance(value, (tuple, list, set)):
|
||||||
|
for child in value:
|
||||||
|
yield from visit(child)
|
||||||
|
|
||||||
|
for value in vars(module).values():
|
||||||
|
yield from visit(value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_and_english_translation_are_identical():
|
||||||
|
assert _load("strings.json") == _load("translations/en.json")
|
||||||
|
|
||||||
|
|
||||||
|
def test_english_and_dutch_catalog_topology_and_placeholders_match():
|
||||||
|
english = _load("translations/en.json")
|
||||||
|
dutch = _load("translations/nl.json")
|
||||||
|
assert _topology(english) == _topology(dutch)
|
||||||
|
|
||||||
|
dutch_strings = dict(_walk_strings(dutch))
|
||||||
|
for path, value in _walk_strings(english):
|
||||||
|
assert _placeholders(value) == _placeholders(dutch_strings[path]), path
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_translatable_descriptor_has_an_entity_catalog_entry():
|
||||||
|
entity_strings = _load("strings.json")["entity"]
|
||||||
|
missing = []
|
||||||
|
for desc in _all_descriptions():
|
||||||
|
translation_key = desc.translation_key
|
||||||
|
if callable(translation_key):
|
||||||
|
# Runtime table resolvers use the generic name-only fallback plus
|
||||||
|
# the static tables currently documented by this integration.
|
||||||
|
continue
|
||||||
|
if translation_key is None and desc.name is not None:
|
||||||
|
translation_key = desc.key
|
||||||
|
if translation_key is None:
|
||||||
|
continue # Main fan entity: device name + HA fan translations.
|
||||||
|
platform = PLATFORM_OF[type(desc)]
|
||||||
|
if translation_key not in entity_strings.get(platform, {}):
|
||||||
|
missing.append((platform, desc.key, translation_key))
|
||||||
|
assert missing == []
|
||||||
|
|
||||||
|
select_strings = entity_strings["select"]
|
||||||
|
for key in ("cycle", "washer_cycle_table_02", "dryer_cycle_table_03"):
|
||||||
|
assert key in select_strings
|
||||||
|
|
||||||
|
|
||||||
|
def test_select_state_normalization_is_synchronized_with_catalog():
|
||||||
|
select_strings = _load("strings.json")["entity"]["select"]
|
||||||
|
translated = {
|
||||||
|
key: frozenset(value["state"])
|
||||||
|
for key, value in select_strings.items()
|
||||||
|
if "state" in value
|
||||||
|
}
|
||||||
|
assert TRANSLATED_SELECT_STATES == translated
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_entity_state_translation_keys_are_lowercase():
|
||||||
|
entity_strings = _load("strings.json")["entity"]
|
||||||
|
for platform in entity_strings.values():
|
||||||
|
for translation in platform.values():
|
||||||
|
for state_key in translation.get("state", {}):
|
||||||
|
assert state_key == state_key.lower()
|
||||||
@@ -64,16 +64,15 @@ class TestWasherCourse:
|
|||||||
"""Table-scoped (issue: course codes aren't guaranteed consistent
|
"""Table-scoped (issue: course codes aren't guaranteed consistent
|
||||||
across board generations sharing /course/vs/0 -- FlexWash's older
|
across board generations sharing /course/vs/0 -- FlexWash's older
|
||||||
board reports Table_00, not the Table_02 every washer_cycle_table_02
|
board reports Table_00, not the Table_02 every washer_cycle_table_02
|
||||||
name was confirmed against) -- see laundry.cycle_select. The key is
|
name was confirmed against) -- see laundry.cycle_select. Only a
|
||||||
built from whatever table the device reports, not gated against a
|
verified table gets table-specific state translations."""
|
||||||
hardcoded 'known good' value."""
|
|
||||||
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == 'cycle')
|
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == 'cycle')
|
||||||
assert callable(desc.translation_key)
|
assert callable(desc.translation_key)
|
||||||
table_02 = {'/st/washercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_02'}}
|
table_02 = {'/st/washercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_02'}}
|
||||||
assert desc.translation_key(table_02) == 'washer_cycle_table_02'
|
assert desc.translation_key(table_02) == 'washer_cycle_table_02'
|
||||||
table_00 = {'/st/washercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_00'}}
|
table_00 = {'/st/washercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_00'}}
|
||||||
assert desc.translation_key(table_00) == 'washer_cycle_table_00'
|
assert desc.translation_key(table_00) == 'cycle'
|
||||||
assert desc.translation_key({}) is None
|
assert desc.translation_key({}) == 'cycle'
|
||||||
|
|
||||||
def test_reads_raw_course_code_from_options_array(self):
|
def test_reads_raw_course_code_from_options_array(self):
|
||||||
"""rep_fn returns the raw device code; display names come from
|
"""rep_fn returns the raw device code; display names come from
|
||||||
@@ -206,14 +205,11 @@ class TestDetergentSoftenerDosing:
|
|||||||
assert self._desc('detergent_quantity').rep_fn(rep) == '7'
|
assert self._desc('detergent_quantity').rep_fn(rep) == '7'
|
||||||
|
|
||||||
def test_translation_keys(self):
|
def test_translation_keys(self):
|
||||||
"""detergent_quantity and softener_quantity share one translation_key
|
"""Each entity gets its own translated name and state vocabulary."""
|
||||||
(same 00-03 -> None/Low/Medium/High vocabulary on both dispensers,
|
assert self._desc('detergent_quantity').translation_key == 'detergent_quantity'
|
||||||
same shape as fridge.py's shared 'brightness_level' key); hardness
|
assert self._desc('softener_quantity').translation_key == 'softener_quantity'
|
||||||
and concentration each have their own since their labels differ."""
|
assert self._desc('detergent_water_hardness').translation_key == 'detergent_water_hardness'
|
||||||
assert self._desc('detergent_quantity').translation_key == 'washer_dosing_quantity'
|
assert self._desc('softener_concentration').translation_key == 'softener_concentration'
|
||||||
assert self._desc('softener_quantity').translation_key == 'washer_dosing_quantity'
|
|
||||||
assert self._desc('detergent_water_hardness').translation_key == 'washer_detergent_water_hardness'
|
|
||||||
assert self._desc('softener_concentration').translation_key == 'washer_softener_concentration'
|
|
||||||
|
|
||||||
def test_quantity_and_hardness_options_decode_supported_list(self):
|
def test_quantity_and_hardness_options_decode_supported_list(self):
|
||||||
assert self._desc('detergent_quantity').options(_DOSING_RESOURCES) == ['00', '01', '02', '03']
|
assert self._desc('detergent_quantity').options(_DOSING_RESOURCES) == ['00', '01', '02', '03']
|
||||||
@@ -359,8 +355,11 @@ class TestWashOptionToggleValidation:
|
|||||||
|
|
||||||
def test_rejected_on_an_unsupported_course(self):
|
def test_rejected_on_an_unsupported_course(self):
|
||||||
rep = {'x.com.samsung.da.options': ['Course_1C', _BUBBLE_SOAK_SET]}
|
rep = {'x.com.samsung.da.options': ['Course_1C', _BUBBLE_SOAK_SET]}
|
||||||
msg = self._desc('bubble_soak').validate_fn('On', rep, _EDIT_COURSE_RESOURCES)
|
issue = self._desc('bubble_soak').validate_fn(
|
||||||
assert msg == "Bubble soak isn't available on the selected cycle."
|
'On', rep, _EDIT_COURSE_RESOURCES
|
||||||
|
)
|
||||||
|
assert issue.translation_key == 'bubble_soak_unavailable_for_cycle'
|
||||||
|
assert issue.translation_placeholders == {}
|
||||||
|
|
||||||
def test_pre_wash_and_intensive_use_their_own_availableset_field(self):
|
def test_pre_wash_and_intensive_use_their_own_availableset_field(self):
|
||||||
rep = {'x.com.samsung.da.options': ['Course_30', _PRE_WASH_AVAILABLE_SET]}
|
rep = {'x.com.samsung.da.options': ['Course_30', _PRE_WASH_AVAILABLE_SET]}
|
||||||
|
|||||||
Reference in New Issue
Block a user