Files
esphome/tests/component_tests/lvgl/test_layout_update.py
T

209 lines
8.0 KiB
Python

"""Tests for updating LVGL layout options via the update actions.
The ``lvgl.update`` and ``lvgl.widget.update`` (and per-widget
``lvgl.<widget>.update``) actions can change a container's layout *options* at
runtime. The layout ``type`` and the grid ``grid_rows``/``grid_columns``
structure are fixed at widget creation (they determine the cells/options
available to child widgets), so only the simple style options - those applied
via ``lv_obj_set_style_...`` calls - may be changed.
These tests cover both the ``layout_validator`` (schema/normalisation) and the
generated C++ for each target: a widget, the active screen (top-level
``lvgl.update``) and the display layers.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from voluptuous import Invalid
from esphome.__main__ import generate_cpp_contents
from esphome.components.lvgl.defines import TYPE_FLEX, TYPE_GRID, get_lv_uses
from esphome.components.lvgl.layout import layout_validator
from esphome.config import read_config
from esphome.core import CORE
# ---------------------------------------------------------------------------
# layout_validator - schema and normalisation
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"value,expected",
[
({"flex_flow": "row"}, {"flex_flow": "LV_FLEX_FLOW_ROW"}),
({"flex_align_main": "center"}, {"flex_align_main": "LV_FLEX_ALIGN_CENTER"}),
({"flex_align_cross": "end"}, {"flex_align_cross": "LV_FLEX_ALIGN_END"}),
(
{"grid_column_align": "space_between"},
{"grid_column_align": "LV_GRID_ALIGN_SPACE_BETWEEN"},
),
({"grid_row_align": "center"}, {"grid_row_align": "LV_GRID_ALIGN_CENTER"}),
({"pad_row": "7px"}, {"pad_row": 7}),
({"pad_column": "5px"}, {"pad_column": 5}),
],
)
def test_layout_validator_normalises_options(value: dict, expected: dict) -> None:
"""Each supported option is accepted and normalised to its LVGL form."""
assert layout_validator(value) == expected
def test_layout_validator_accepts_multiple_options() -> None:
"""Several options may be combined in one update."""
result = layout_validator(
{"flex_flow": "column", "flex_align_main": "center", "pad_row": "4px"}
)
assert result == {
"flex_flow": "LV_FLEX_FLOW_COLUMN",
"flex_align_main": "LV_FLEX_ALIGN_CENTER",
"pad_row": 4,
}
@pytest.mark.parametrize(
"value",
[
{"type": "flex"},
{"type": "grid", "grid_column_align": "center"},
{"grid_rows": 3},
{"grid_columns": ["fr(1)"]},
{"grid_rows": [1, 2], "flex_flow": "row"},
],
)
def test_layout_validator_rejects_structural_keys(value: dict) -> None:
"""The layout type and grid structure are fixed at creation and must not
be changeable via an update action."""
with pytest.raises(Invalid, match="extra keys not allowed"):
layout_validator(value)
def test_layout_validator_rejects_empty() -> None:
"""An update must specify at least one layout option."""
with pytest.raises(Invalid, match="at least one layout option"):
layout_validator({})
def test_layout_validator_registers_flex_use() -> None:
"""Validating a flex option registers the flex feature so LV_USE_FLEX is
emitted even when the option is set solely via an update action."""
layout_validator({"flex_flow": "row"})
assert TYPE_FLEX in get_lv_uses()
def test_layout_validator_registers_grid_use() -> None:
"""Validating a grid option registers the grid feature."""
layout_validator({"grid_column_align": "center"})
assert TYPE_GRID in get_lv_uses()
def test_pad_only_update_registers_no_layout_use() -> None:
"""Padding options belong to both layout types, so they alone do not force
either feature on."""
layout_validator({"pad_row": "4px"})
uses = get_lv_uses()
assert TYPE_FLEX not in uses
assert TYPE_GRID not in uses
# ---------------------------------------------------------------------------
# Generated C++ for the update actions
# ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def main_cpp(request: pytest.FixtureRequest) -> str:
"""Generate the C++ output for the shared layout-update YAML config once
per module (codegen is relatively expensive)."""
config_path = Path(request.fspath).parent / "config" / "layout_update_test.yaml"
original_path = CORE.config_path
try:
CORE.config_path = config_path
CORE.config = read_config({})
generate_cpp_contents(CORE.config)
return CORE.cpp_global_section + CORE.cpp_main_section
finally:
CORE.config_path = original_path
CORE.reset()
def test_widget_flex_update_applies_partial_options(main_cpp: str) -> None:
"""``lvgl.widget.update`` changes only the flex options that are specified,
via the appropriate ``lv_obj_set_style_...``/``lv_obj_set_flex_flow``
calls on the target widget."""
assert "lv_obj_set_flex_flow(flex_box, LV_FLEX_FLOW_COLUMN)" in main_cpp
assert (
"lv_obj_set_style_flex_main_place(flex_box, LV_FLEX_ALIGN_CENTER, LV_STATE_DEFAULT)"
in main_cpp
)
assert (
"lv_obj_set_style_flex_cross_place(flex_box, LV_FLEX_ALIGN_END, LV_STATE_DEFAULT)"
in main_cpp
)
assert "lv_obj_set_style_pad_row(flex_box, 7, LV_STATE_DEFAULT)" in main_cpp
def test_widget_flex_update_does_not_change_type(main_cpp: str) -> None:
"""The update must not re-establish the layout type: ``lv_obj_set_layout``
is emitted once (at creation) and never from the update action."""
assert main_cpp.count("lv_obj_set_layout(flex_box,") == 1
def test_widget_flex_update_is_partial(main_cpp: str) -> None:
"""An option that was not specified in the update (the track placement) is
only set at creation, not by the partial update."""
assert main_cpp.count("lv_obj_set_style_flex_track_place(flex_box,") == 1
def test_widget_grid_update_applies_alignments(main_cpp: str) -> None:
"""``lvgl.widget.update`` on a grid container changes its alignment
options without touching the grid structure."""
assert (
"lv_obj_set_style_grid_column_align(grid_box, LV_GRID_ALIGN_SPACE_BETWEEN, "
"LV_STATE_DEFAULT)" in main_cpp
)
assert (
"lv_obj_set_style_grid_row_align(grid_box, LV_GRID_ALIGN_CENTER, LV_STATE_DEFAULT)"
in main_cpp
)
def test_grid_update_does_not_regenerate_descriptor_arrays(main_cpp: str) -> None:
"""The grid row/column descriptor arrays are structural and generated once
at creation; an update must not regenerate them."""
assert main_cpp.count("grid_box_row_dsc") != 0
# The descriptor array is declared once and referenced once at creation.
assert main_cpp.count("grid_box_row_dsc") == main_cpp.count("grid_box_column_dsc")
assert "lv_obj_set_layout(grid_box," in main_cpp
assert main_cpp.count("lv_obj_set_layout(grid_box,") == 1
def test_top_level_layout_targets_active_screen(main_cpp: str) -> None:
"""A top-level ``lvgl.update: { layout: ... }`` applies to the active
screen, not to the LVGL component object."""
assert (
"lv_obj_set_flex_flow(lvgl_id->get_screen_active(), LV_FLEX_FLOW_COLUMN)"
in main_cpp
)
assert (
"lv_obj_set_style_pad_column(lvgl_id->get_screen_active(), 5, LV_STATE_DEFAULT)"
in main_cpp
)
def test_top_layer_layout_applied(main_cpp: str) -> None:
"""A layout under ``top_layer`` is applied to the display's top layer."""
assert "lv_display_get_layer_top(lvgl_id->get_disp())" in main_cpp
assert "lv_obj_set_flex_flow(top_layer_VAR_, LV_FLEX_FLOW_ROW)" in main_cpp
def test_bottom_layer_styling_applied(main_cpp: str) -> None:
"""A ``bottom_layer`` style update generates code (previously the layer
keys of ``lvgl.update`` were silently ignored)."""
assert "lv_display_get_layer_bottom(lvgl_id->get_disp())" in main_cpp
assert (
"lv_obj_set_style_bg_color(bottom_layer_VAR_, lv_color_make(18, 52, 86), "
"LV_PART_MAIN)" in main_cpp
)