|
| 1 | +"""Options flow tests. |
| 2 | +
|
| 3 | +These exist because of a specific failure. The "Max Register Block Size" selector |
| 4 | +shipped in v1.2.0 declared as `vol.In({0: "Auto", 25: "25 registers", ...})` — keyed by |
| 5 | +integers, with `default=0`. **The option could never be saved.** |
| 6 | +
|
| 7 | +It survived four releases. The read path was wired correctly, so code inspection looked |
| 8 | +fine; the value simply never reached it. Two users found it independently, as two |
| 9 | +different symptoms — "nothing is selected" (#360) and "the option had zero effect" |
| 10 | +(#367) — and I initially told the second one the code was correct. |
| 11 | +
|
| 12 | +Nothing in the HA-free suite could have caught it: the defect is in a voluptuous schema |
| 13 | +that only misbehaves when Home Assistant renders and submits it. That is the entire |
| 14 | +argument for this directory existing. |
| 15 | +""" |
| 16 | +from __future__ import annotations |
| 17 | + |
| 18 | +import pytest |
| 19 | +from homeassistant.core import HomeAssistant |
| 20 | + |
| 21 | +from custom_components.growatt_modbus.const import BLOCK_SIZE_OPTIONS |
| 22 | + |
| 23 | + |
| 24 | +async def _open_options(hass: HomeAssistant, entry): |
| 25 | + entry.add_to_hass(hass) |
| 26 | + assert await hass.config_entries.async_setup(entry.entry_id) |
| 27 | + await hass.async_block_till_done() |
| 28 | + return await hass.config_entries.options.async_init(entry.entry_id) |
| 29 | + |
| 30 | + |
| 31 | +@pytest.mark.parametrize("label", list(BLOCK_SIZE_OPTIONS)) |
| 32 | +async def test_every_block_size_label_can_be_saved( |
| 33 | + hass: HomeAssistant, mock_entry, bypass_connection, label |
| 34 | +): |
| 35 | + """The regression, stated directly: each offered choice must persist. |
| 36 | +
|
| 37 | + Under the old schema this failed for every value — which is what made the option |
| 38 | + inert rather than merely awkward. |
| 39 | + """ |
| 40 | + result = await _open_options(hass, mock_entry) |
| 41 | + |
| 42 | + result = await hass.config_entries.options.async_configure( |
| 43 | + result["flow_id"], |
| 44 | + user_input={ |
| 45 | + "device_name": "Growatt Test", |
| 46 | + "inverter_series": "MIN (7-10kW)", |
| 47 | + "scan_interval": 60, |
| 48 | + "offline_scan_interval": 300, |
| 49 | + "invert_grid_power": False, |
| 50 | + "invert_battery_power": False, |
| 51 | + "battery_voltage_range": "Auto-detect", |
| 52 | + "modbus_delay": 250, |
| 53 | + "max_block_size": label, |
| 54 | + }, |
| 55 | + ) |
| 56 | + await hass.async_block_till_done() |
| 57 | + |
| 58 | + assert result["type"] == "create_entry" |
| 59 | + assert mock_entry.options["max_block_size"] == label |
| 60 | + |
| 61 | + |
| 62 | +async def test_saved_block_size_reaches_the_read_path( |
| 63 | + hass: HomeAssistant, mock_entry, bypass_connection |
| 64 | +): |
| 65 | + """Saving is only half of it — the value must arrive at the client. |
| 66 | +
|
| 67 | + v1.2.0 wired this end correctly while the form end was broken, so verifying only the |
| 68 | + wiring gave a false positive. This asserts the whole chain. |
| 69 | + """ |
| 70 | + from custom_components.growatt_modbus.const import resolve_block_size |
| 71 | + |
| 72 | + result = await _open_options(hass, mock_entry) |
| 73 | + await hass.config_entries.options.async_configure( |
| 74 | + result["flow_id"], |
| 75 | + user_input={ |
| 76 | + "device_name": "Growatt Test", |
| 77 | + "inverter_series": "MIN (7-10kW)", |
| 78 | + "scan_interval": 60, |
| 79 | + "offline_scan_interval": 300, |
| 80 | + "invert_grid_power": False, |
| 81 | + "invert_battery_power": False, |
| 82 | + "battery_voltage_range": "Auto-detect", |
| 83 | + "modbus_delay": 250, |
| 84 | + "max_block_size": "25 registers", |
| 85 | + }, |
| 86 | + ) |
| 87 | + await hass.async_block_till_done() |
| 88 | + |
| 89 | + assert resolve_block_size(mock_entry.options["max_block_size"]) == 25 |
| 90 | + |
| 91 | + |
| 92 | +async def test_options_form_opens_with_a_valid_default( |
| 93 | + hass: HomeAssistant, mock_entry, bypass_connection |
| 94 | +): |
| 95 | + """A default that matches no offered choice renders as nothing selected. |
| 96 | +
|
| 97 | + That was the visible half of the bug (#360) — and because the field is Required, |
| 98 | + an unselected form also refuses to submit, blocking *every* option on the page. |
| 99 | + """ |
| 100 | + result = await _open_options(hass, mock_entry) |
| 101 | + assert result["type"] == "form" |
| 102 | + assert result["errors"] in (None, {}) |
| 103 | + |
| 104 | + |
| 105 | +async def test_unrelated_option_can_be_changed_without_touching_block_size( |
| 106 | + hass: HomeAssistant, mock_entry, bypass_connection |
| 107 | +): |
| 108 | + """The real user impact: a broken selector locked the whole form. |
| 109 | +
|
| 110 | + #360 could not change scan interval, because the invalid block-size default failed |
| 111 | + validation for the entire submission. |
| 112 | + """ |
| 113 | + result = await _open_options(hass, mock_entry) |
| 114 | + result = await hass.config_entries.options.async_configure( |
| 115 | + result["flow_id"], |
| 116 | + user_input={ |
| 117 | + "device_name": "Growatt Test", |
| 118 | + "inverter_series": "MIN (7-10kW)", |
| 119 | + "scan_interval": 120, |
| 120 | + "offline_scan_interval": 300, |
| 121 | + "invert_grid_power": False, |
| 122 | + "invert_battery_power": False, |
| 123 | + "battery_voltage_range": "Auto-detect", |
| 124 | + "modbus_delay": 250, |
| 125 | + "max_block_size": "Auto (recommended)", |
| 126 | + }, |
| 127 | + ) |
| 128 | + await hass.async_block_till_done() |
| 129 | + |
| 130 | + assert result["type"] == "create_entry" |
| 131 | + assert mock_entry.options["scan_interval"] == 120 |
0 commit comments