mirror of
https://github.com/esphome/esphome.git
synced 2026-10-01 08:50:22 +00:00
[lvgl] Use contextvars to improve lvgl scheduling (#19242)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e111f1130c
commit
5baad05926
@@ -0,0 +1,28 @@
|
||||
esphome:
|
||||
name: test-keyboard-before-textarea
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- keyboard:
|
||||
id: kb
|
||||
textarea: ta
|
||||
- textarea:
|
||||
id: ta
|
||||
@@ -0,0 +1,34 @@
|
||||
esphome:
|
||||
name: test-keyboard-update-textarea
|
||||
on_boot:
|
||||
- lvgl.keyboard.update:
|
||||
id: kb
|
||||
textarea: ta2
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- keyboard:
|
||||
id: kb
|
||||
textarea: ta1
|
||||
- textarea:
|
||||
id: ta1
|
||||
- textarea:
|
||||
id: ta2
|
||||
@@ -0,0 +1,35 @@
|
||||
esphome:
|
||||
name: test-multi-instance-theme
|
||||
|
||||
host:
|
||||
mac_address: "62:23:45:AF:B3:DD"
|
||||
|
||||
display:
|
||||
- platform: snapshot
|
||||
id: sdl0
|
||||
dimensions:
|
||||
width: 100
|
||||
height: 100
|
||||
- platform: snapshot
|
||||
id: sdl1
|
||||
dimensions:
|
||||
width: 100
|
||||
height: 100
|
||||
|
||||
lvgl:
|
||||
- id: lvgl_0
|
||||
displays: sdl0
|
||||
widgets:
|
||||
- label:
|
||||
id: label_0
|
||||
text: "First"
|
||||
|
||||
- id: lvgl_1
|
||||
displays: sdl1
|
||||
theme:
|
||||
label:
|
||||
bg_color: 0xFF0000
|
||||
widgets:
|
||||
- label:
|
||||
id: label_1
|
||||
text: "Second"
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Regression test: a keyboard: declared before its textarea: sibling must
|
||||
still get attached to it, and only after both widgets exist.
|
||||
|
||||
attach_textareas() runs as a deferred pass, after every widget (across every
|
||||
LVGL instance) is created, so it must emit the attach call after the
|
||||
keyboard's own creation statement, not inline during widget creation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "keyboard_before_textarea_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_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_keyboard_and_textarea_are_created(main_cpp: str) -> None:
|
||||
assert "lv_keyboard_create(" in main_cpp
|
||||
assert "lv_textarea_create(" in main_cpp
|
||||
|
||||
|
||||
def test_attach_call_runs_after_keyboard_and_textarea_are_created(
|
||||
main_cpp: str,
|
||||
) -> None:
|
||||
attach_index = main_cpp.find("lv_keyboard_set_textarea(kb->obj, ta);")
|
||||
assert attach_index != -1, "keyboard was never attached to its textarea"
|
||||
assert attach_index > main_cpp.find("lv_keyboard_create(")
|
||||
assert attach_index > main_cpp.find("lv_textarea_create(")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Regression test: lvgl.keyboard.update must be able to change which
|
||||
textarea a keyboard is attached to.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "keyboard_update_textarea_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_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_keyboard_update_changes_textarea(main_cpp: str) -> None:
|
||||
assert "lv_keyboard_set_textarea(kb->obj, ta2);" in main_cpp
|
||||
@@ -286,7 +286,7 @@ def test_add_text_with_index_moves_before_firing_on_add(main_cpp: str) -> None:
|
||||
|
||||
|
||||
def test_add_button_with_checkable_flag(main_cpp: str) -> None:
|
||||
assert "lv_obj_t *dyn_button_VAR_ = lv_btn_create(test_list);" in main_cpp
|
||||
assert "lv_obj_t *dyn_button_VAR_ = lv_button_create(test_list);" in main_cpp
|
||||
assert (
|
||||
"lv_obj_add_flag(dyn_button_VAR_, (lv_obj_flag_t)(LV_OBJ_FLAG_CHECKABLE));"
|
||||
in main_cpp
|
||||
@@ -310,7 +310,7 @@ def test_add_nested_hierarchy_with_compound_child(main_cpp: str) -> None:
|
||||
assert (
|
||||
"lv_obj_t *dyn_label_1_VAR_ = lv_label_create(dyn_obj_VAR_);\n"
|
||||
" lv_obj_add_style(dyn_label_1_VAR_, _lv_theme_style_label_main_default, "
|
||||
"(lv_state_t)(LV_PART_MAIN));\n"
|
||||
"LV_PART_MAIN);\n"
|
||||
' lv_label_set_text(dyn_label_1_VAR_, "Nested");'
|
||||
) in main_cpp
|
||||
|
||||
@@ -321,7 +321,7 @@ def test_add_applies_theme_styles_to_dynamic_widget(main_cpp: str) -> None:
|
||||
"""
|
||||
assert (
|
||||
"lv_obj_add_style(dyn_label_1_VAR_, _lv_theme_style_label_main_default, "
|
||||
"(lv_state_t)(LV_PART_MAIN));"
|
||||
"LV_PART_MAIN);"
|
||||
) in main_cpp
|
||||
assert "LvDropdownType *dyn_dropdown_1_VAR_ = new LvDropdownType();" in main_cpp
|
||||
assert "lv_dropdown_create(dyn_obj_VAR_)" in main_cpp
|
||||
@@ -343,7 +343,7 @@ def test_add_nested_same_type_child_does_not_shadow_parent(main_cpp: str) -> Non
|
||||
assert (
|
||||
"lv_obj_t *dyn_label_2_VAR_ = lv_label_create(dyn_obj_1_VAR_);\n"
|
||||
" lv_obj_add_style(dyn_label_2_VAR_, _lv_theme_style_label_main_default, "
|
||||
"(lv_state_t)(LV_PART_MAIN));\n"
|
||||
"LV_PART_MAIN);\n"
|
||||
' lv_label_set_text(dyn_label_2_VAR_, "Grandchild");'
|
||||
) in main_cpp
|
||||
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
"""Regression test: on_add:/on_remove: containing an lvgl action must not deadlock.
|
||||
|
||||
ListType.to_code() used to build the on_add/on_remove automations directly, during
|
||||
widget creation. Every lvgl action's to_code awaits wait_for_widgets(), which only
|
||||
resolves once *all* widgets - including the list itself - have finished being
|
||||
created. Building an automation containing an lvgl action from inside that same
|
||||
widget-creation walk therefore could never complete: codegen deadlocked with
|
||||
"Circular dependency detected!". Fixed by deferring the actual build_automation()
|
||||
call to finish_list_triggers(), run after set_widgets_completed(True) - and,
|
||||
critically, before generate_triggers(), which is what processes other widgets'
|
||||
on_click etc. automations that might reference this list (e.g. via lvgl.list.add),
|
||||
and which therefore need the list's own on_add/on_remove triggers to already exist.
|
||||
"""
|
||||
"""Regression test: on_add:/on_remove: containing an lvgl action must not deadlock."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Regression test: a widget's theme style must be attached regardless of
|
||||
which LVGL instance declares theme: and which instance's widgets are built
|
||||
first.
|
||||
|
||||
theme_to_code() runs once per LVGL instance, interleaved with that instance's
|
||||
own add_widgets(). A widget built by an earlier instance, before any instance
|
||||
has declared theming for its type, must still pick up a later instance's
|
||||
theme: declaration for that type - including on the later instance's own
|
||||
widgets, which get_widget_theme_styles()'s memoisation can poison too.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "multi_instance_theme_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_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_earlier_instance_widget_is_themed(main_cpp: str) -> None:
|
||||
assert "lv_obj_add_style(label_0, _lv_theme_style_label_main_default," in main_cpp
|
||||
|
||||
|
||||
def test_later_instance_widget_is_themed(main_cpp: str) -> None:
|
||||
assert "lv_obj_add_style(label_1, _lv_theme_style_label_main_default," in main_cpp
|
||||
@@ -42,6 +42,13 @@ script:
|
||||
label:
|
||||
text: row
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- lvgl.theme.update:
|
||||
obj:
|
||||
border_width: 2
|
||||
|
||||
lvgl:
|
||||
id: lvgl_id
|
||||
rotation: 90
|
||||
@@ -1351,6 +1358,17 @@ lvgl:
|
||||
id: checkbox_id
|
||||
text: Checkbox
|
||||
align: bottom_right
|
||||
- list:
|
||||
id: list_a
|
||||
on_add:
|
||||
- lvgl.list.add:
|
||||
id: list_b # fires list_b's on_add
|
||||
label: {text: x}
|
||||
- list:
|
||||
id: list_b
|
||||
on_add:
|
||||
- logger.log: added
|
||||
|
||||
- list:
|
||||
id: test_list_id
|
||||
align: top_right
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Regression test: action_to_code(): Confirms two
|
||||
actions whose own processing each suspends mid-lambda, waiting for an ID, don't
|
||||
corrupt each other's LambdaContext when interleaved.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.lvgl.automation import action_to_code
|
||||
from esphome.components.lvgl.lvcode import lv_add
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import RawExpression, TemplateArguments
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_action_to_code_survives_interleaved_suspended_contexts(
|
||||
setup_core,
|
||||
) -> None:
|
||||
later_id_a = ID("later_var_a", False, cg.int_)
|
||||
later_id_b = ID("later_var_b", False, cg.int_)
|
||||
action_type = ACTION_REGISTRY["lvgl.list.add"].type_id
|
||||
|
||||
async def action_a(_widget) -> None:
|
||||
value = await cg.get_variable(later_id_a)
|
||||
lv_add(RawExpression(f"action_a_marker({value})"))
|
||||
|
||||
async def action_b(_widget) -> None:
|
||||
value = await cg.get_variable(later_id_b)
|
||||
lv_add(RawExpression(f"action_b_marker({value})"))
|
||||
|
||||
async def run_action_a() -> None:
|
||||
action_id = ID("test_action_a", is_declaration=True, type=action_type)
|
||||
await action_to_code([None], action_a, action_id, TemplateArguments(), [])
|
||||
|
||||
async def run_action_b() -> None:
|
||||
action_id = ID("test_action_b", is_declaration=True, type=action_type)
|
||||
await action_to_code([None], action_b, action_id, TemplateArguments(), [])
|
||||
|
||||
async def define_later_ids() -> None:
|
||||
# Both actions are still suspended, mid-LambdaContext, when this runs -
|
||||
# resolving both at once lets each resume while the other's context is
|
||||
# still open, rather than one finishing before the other starts.
|
||||
cg.new_variable(later_id_a, RawExpression("1"))
|
||||
cg.new_variable(later_id_b, RawExpression("2"))
|
||||
|
||||
CORE.add_job(run_action_a)
|
||||
CORE.add_job(run_action_b)
|
||||
CORE.add_job(define_later_ids)
|
||||
CORE.flush_tasks()
|
||||
|
||||
text = "\n".join(str(s) for s in CORE.main_statements)
|
||||
assert "action_a_marker(later_var_a)" in text
|
||||
assert "action_b_marker(later_var_b)" in text
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Tests for get_part_state_selector()'s three branches."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from esphome.components.lvgl.defines import get_part_state_selector
|
||||
|
||||
|
||||
def test_default_state_returns_bare_part() -> None:
|
||||
assert str(get_part_state_selector("main", "default")) == "LV_PART_MAIN"
|
||||
assert str(get_part_state_selector("knob", "default")) == "LV_PART_KNOB"
|
||||
|
||||
|
||||
def test_main_part_with_non_default_state_returns_bare_state() -> None:
|
||||
assert str(get_part_state_selector("main", "pressed")) == "LV_STATE_PRESSED"
|
||||
|
||||
|
||||
def test_non_main_part_with_non_default_state_combines_both() -> None:
|
||||
assert str(get_part_state_selector("knob", "pressed")) == (
|
||||
"(static_cast<lv_style_selector_t>(LV_STATE_PRESSED) | "
|
||||
"static_cast<lv_style_selector_t>(LV_PART_KNOB))"
|
||||
)
|
||||
|
||||
|
||||
def test_accepts_already_prefixed_part_and_state() -> None:
|
||||
assert (
|
||||
str(get_part_state_selector("LV_PART_MAIN", "LV_STATE_DEFAULT"))
|
||||
== "LV_PART_MAIN"
|
||||
)
|
||||
assert (
|
||||
str(get_part_state_selector("LV_PART_KNOB", "LV_STATE_PRESSED"))
|
||||
== "(static_cast<lv_style_selector_t>(LV_STATE_PRESSED) | "
|
||||
"static_cast<lv_style_selector_t>(LV_PART_KNOB))"
|
||||
)
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Regression test: an lvgl.list.add action must fire a list's on_add trigger
|
||||
even if it reaches _fire_on_add() before finish_list_triggers() has built that
|
||||
list's Trigger Pvariable.
|
||||
|
||||
ListType.on_create() only records on_add/on_remove configs (via
|
||||
_declare_list_triggers()) - finish_list_triggers() is what actually builds the
|
||||
Trigger Pvariables from them. An lvgl.list.add action for a list can be
|
||||
scheduled as part of a different component's own to_code() coroutine, entirely
|
||||
independent of lvgl's own, so it can reach _fire_on_add() before
|
||||
finish_list_triggers() has run for that list. _fire_on_add()/_fire_on_remove()
|
||||
resolve each trigger via cg.get_variable(), which blocks until
|
||||
finish_list_triggers() builds it - regardless of which of the two jobs the
|
||||
scheduler happens to run first.
|
||||
|
||||
This is reproduced deterministically here (no reliance on incidental component
|
||||
priority/scheduling) by scheduling the action's job before finish_list_triggers()
|
||||
on ESPHome's own coroutine scheduler: without cg.get_variable()'s wait, the
|
||||
action job would run to completion first and observe the trigger as not yet
|
||||
built.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
from esphome.components.lvgl.lvcode import LvContext
|
||||
from esphome.components.lvgl.schemas import container_schema
|
||||
from esphome.components.lvgl.widgets import Widget, widget_to_code
|
||||
from esphome.components.lvgl.widgets.lv_list import (
|
||||
CONF_ON_ADD,
|
||||
_get_list_triggers,
|
||||
finish_list_triggers,
|
||||
list_spec,
|
||||
)
|
||||
from esphome.const import CONF_AUTOMATION_ID, CONF_THEN, CONF_TRIGGER_ID, CONF_TYPE_ID
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArguments
|
||||
from esphome.yaml_util import make_data_base
|
||||
|
||||
|
||||
def _statements() -> list[str]:
|
||||
return [str(s) for s in CORE.main_statements]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_add_action_running_before_finish_list_triggers_still_fires_on_add(
|
||||
setup_core,
|
||||
) -> None:
|
||||
config = container_schema(list_spec)(
|
||||
{
|
||||
"id": "test_list",
|
||||
CONF_ON_ADD: [{"lambda": make_data_base("return;")}],
|
||||
}
|
||||
)
|
||||
# Auto-generated IDs (trigger/automation/action) are normally resolved to
|
||||
# unique names by esphome's full config pass before code generation; do
|
||||
# that by hand here since this test only exercises the widget/trigger
|
||||
# codegen slice in isolation.
|
||||
automation_conf = config[CONF_ON_ADD][0]
|
||||
automation_conf[CONF_TRIGGER_ID].resolve([])
|
||||
automation_conf[CONF_AUTOMATION_ID].resolve([])
|
||||
automation_conf[CONF_THEN][0][CONF_TYPE_ID].resolve([])
|
||||
|
||||
parent = MockObj("parent_obj")
|
||||
async with LvContext():
|
||||
await widget_to_code(config, list_spec, parent)
|
||||
|
||||
# Schedule the lvgl.list.add action's job before finish_list_triggers()'s -
|
||||
# mirroring an action that lives in a different component's automation than
|
||||
# lvgl's own to_code(), which can reach this action before lvgl gets to build
|
||||
# this list's on_add/on_remove triggers.
|
||||
entry = ACTION_REGISTRY["lvgl.list.add"]
|
||||
add_config = entry.schema({"id": "test_list", "label": {"text": "row"}})
|
||||
action_id = ID("test_list_add_action", is_declaration=True, type=entry.type_id)
|
||||
|
||||
async def run_add_action() -> None:
|
||||
async with LvContext():
|
||||
await entry.coroutine_fun(add_config, action_id, TemplateArguments(), [])
|
||||
|
||||
CORE.add_job(run_add_action)
|
||||
CORE.add_job(finish_list_triggers)
|
||||
CORE.flush_tasks()
|
||||
|
||||
statements = _statements()
|
||||
assert any("->trigger(" in s for s in statements), (
|
||||
"on_add did not fire: the lvgl.list.add action ran before "
|
||||
"finish_list_triggers() built the list's on_add trigger, and "
|
||||
"_fire_on_add() didn't wait for it"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_add_recorded_before_widget_registered(setup_core) -> None:
|
||||
"""Widget.create() is what makes a list visible to get_widgets(), so an
|
||||
action interleaved with its creation could resolve get_widgets() and reach
|
||||
_fire_on_add() right after Widget.create() runs. Its on_add config must
|
||||
already be recorded by then - ListType.on_create() (called before
|
||||
Widget.create()) is what guarantees that, not ListType.to_code() (called
|
||||
after).
|
||||
"""
|
||||
config = container_schema(list_spec)(
|
||||
{
|
||||
"id": "test_list",
|
||||
CONF_ON_ADD: [{"lambda": make_data_base("return;")}],
|
||||
}
|
||||
)
|
||||
automation_conf = config[CONF_ON_ADD][0]
|
||||
automation_conf[CONF_TRIGGER_ID].resolve([])
|
||||
automation_conf[CONF_AUTOMATION_ID].resolve([])
|
||||
automation_conf[CONF_THEN][0][CONF_TYPE_ID].resolve([])
|
||||
|
||||
seen_on_add_counts = []
|
||||
real_create = Widget.create
|
||||
|
||||
def spy_create(name, var, wtype, config=None):
|
||||
seen_on_add_counts.append(len(_get_list_triggers(name).on_add))
|
||||
return real_create(name, var, wtype, config)
|
||||
|
||||
parent = MockObj("parent_obj")
|
||||
with patch.object(Widget, "create", side_effect=spy_create):
|
||||
async with LvContext():
|
||||
await widget_to_code(config, list_spec, parent)
|
||||
|
||||
assert seen_on_add_counts == [1], (
|
||||
"on_add wasn't recorded yet when Widget.create() registered the list"
|
||||
)
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import pytest
|
||||
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
from esphome.components.lvgl.defines import set_widgets_completed
|
||||
from esphome.components.lvgl.lvcode import LvContext
|
||||
from esphome.components.lvgl.schemas import container_schema
|
||||
from esphome.components.lvgl.trigger import generate_triggers
|
||||
@@ -151,7 +150,6 @@ async def test_selected_cell_omitted_entirely_when_not_configured(
|
||||
@pytest.mark.asyncio
|
||||
async def test_cell_update_action_writes_only_the_given_fields(setup_core) -> None:
|
||||
await _create_table({"id": "table_update", "rows": [["a", "b"], ["c", "d"]]})
|
||||
set_widgets_completed(True)
|
||||
# Only inspect statements emitted by the action below, not by creation.
|
||||
before = len(_statements())
|
||||
|
||||
@@ -194,7 +192,6 @@ async def test_on_value_registers_a_value_changed_event_callback(setup_core) ->
|
||||
parent = MockObj("parent_obj")
|
||||
async with LvContext():
|
||||
await widget_to_code(config, table_spec, parent)
|
||||
set_widgets_completed(True)
|
||||
await generate_triggers()
|
||||
|
||||
statements = _statements()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Regression test: an lvgl.theme.update action must still apply its style
|
||||
change even if it runs before theme_to_code() has built that style.
|
||||
|
||||
theme_update_to_code() reads get_theme_widget_map() synchronously and raises
|
||||
cv.Invalid if the requested style isn't there yet - relying on theme_to_code()
|
||||
(which materialises a style for every requested widget/part/state combo) to
|
||||
have always already run. That's guaranteed when the action lives inside the
|
||||
lvgl: block's own automations (same to_code() job, sequential), but not when
|
||||
it's scheduled as part of a different component's own to_code() job - e.g.
|
||||
tests/components/lvgl/lvgl-package.yaml's `esphome: on_boot:` case, which this
|
||||
test reproduces at the scheduler level.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
from esphome.components.lvgl.schemas import theme_update_schema
|
||||
from esphome.components.lvgl.styles import theme_to_code
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import TemplateArguments
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_theme_update_before_theme_to_code_still_applies(setup_core) -> None:
|
||||
add_config = theme_update_schema({"obj": {"border_width": 2}})
|
||||
|
||||
entry = ACTION_REGISTRY["lvgl.theme.update"]
|
||||
action_id = ID("test_theme_update_action", is_declaration=True, type=entry.type_id)
|
||||
|
||||
async def run_update_action() -> None:
|
||||
await entry.coroutine_fun(add_config, action_id, TemplateArguments(), [])
|
||||
|
||||
# Scheduled before theme_to_code()'s job - mirrors the action being reached
|
||||
# from a different component's own to_code() job than lvgl's.
|
||||
CORE.add_job(run_update_action)
|
||||
CORE.add_job(theme_to_code, {})
|
||||
CORE.flush_tasks()
|
||||
|
||||
statements = [str(s) for s in CORE.main_statements]
|
||||
assert any("style_set_border_width" in s for s in statements), (
|
||||
"theme.update's border_width change was never applied"
|
||||
)
|
||||
Reference in New Issue
Block a user