mirror of
https://github.com/esphome/esphome.git
synced 2026-08-29 17:16:45 +00:00
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick+github@koston.org>
207 lines
7.1 KiB
Python
207 lines
7.1 KiB
Python
"""Tests for the LVGL table widget's C++ code generation."""
|
|
|
|
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
|
|
from esphome.components.lvgl.widgets import Widget, widget_to_code
|
|
from esphome.components.lvgl.widgets.table import table_spec
|
|
from esphome.const import (
|
|
CONF_AUTOMATION_ID,
|
|
CONF_ON_VALUE,
|
|
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
|
|
|
|
|
|
async def _create_table(raw_config: dict) -> Widget:
|
|
"""Validate `raw_config` as a table widget and generate its creation code."""
|
|
config = container_schema(table_spec)(raw_config)
|
|
parent = MockObj("parent_obj")
|
|
async with LvContext():
|
|
return await widget_to_code(config, table_spec, parent)
|
|
|
|
|
|
def _statements() -> list[str]:
|
|
return [str(s) for s in CORE.main_statements]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_table_sets_row_and_column_count(setup_core) -> None:
|
|
await _create_table(
|
|
{"id": "table_counts", "rows": [["Name", "Value"], ["Temp", "22.5"]]}
|
|
)
|
|
statements = _statements()
|
|
assert any("lv_table_set_row_count(table_counts->obj, 2)" in s for s in statements)
|
|
assert any(
|
|
"lv_table_set_column_count(table_counts->obj, 2)" in s for s in statements
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_table_writes_cell_values(setup_core) -> None:
|
|
await _create_table({"id": "table_cells", "rows": [["Name", "Value"]]})
|
|
statements = _statements()
|
|
assert any(
|
|
'lv_table_set_cell_value(table_cells->obj, 0, 0, "Name")' in s
|
|
for s in statements
|
|
)
|
|
assert any(
|
|
'lv_table_set_cell_value(table_cells->obj, 0, 1, "Value")' in s
|
|
for s in statements
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_table_sets_cell_control_flags(setup_core) -> None:
|
|
await _create_table(
|
|
{
|
|
"id": "table_ctrl",
|
|
"rows": [
|
|
{
|
|
"cells": [
|
|
{"text": "wide", "merge_right": True},
|
|
{"text": "cropped", "text_crop": True},
|
|
]
|
|
}
|
|
],
|
|
}
|
|
)
|
|
statements = _statements()
|
|
assert any(
|
|
"lv_table_set_cell_ctrl(table_ctrl->obj, 0, 0, LV_TABLE_CELL_CTRL_MERGE_RIGHT)"
|
|
in s
|
|
for s in statements
|
|
)
|
|
assert any(
|
|
"lv_table_set_cell_ctrl(table_ctrl->obj, 0, 1, LV_TABLE_CELL_CTRL_TEXT_CROP)"
|
|
in s
|
|
for s in statements
|
|
)
|
|
# text_crop omitted for cell 0: no clear_cell_ctrl() should be emitted.
|
|
assert not any(
|
|
"table_ctrl->obj, 0, 0, LV_TABLE_CELL_CTRL_TEXT_CROP" in s for s in statements
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pixel_column_width_calls_lvgl_directly(setup_core) -> None:
|
|
await _create_table({"id": "table_px", "columns": [{"width": 96}]})
|
|
statements = _statements()
|
|
assert any(
|
|
"lv_table_set_column_width(table_px->obj, 0, 96)" in s for s in statements
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_percent_column_width_uses_the_dynamic_helper(setup_core) -> None:
|
|
"""Regression test: lv_table_set_column_width() only accepts a literal
|
|
pixel count, so a percentage width must not be passed to it directly -
|
|
it has to go through the LvTableType helper that recomputes it at
|
|
runtime from the table's actual content width.
|
|
"""
|
|
await _create_table({"id": "table_pct", "columns": [{"width": "40%"}]})
|
|
statements = _statements()
|
|
assert any("table_pct->init_column_pct(1)" in s for s in statements)
|
|
assert any("table_pct->add_column_width_pct(0, 40)" in s for s in statements)
|
|
assert not any(
|
|
"lv_table_set_column_width(table_pct->obj, 0" in s for s in statements
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_selected_cell_with_both_indices(setup_core) -> None:
|
|
await _create_table(
|
|
{"id": "table_sel_both", "selected_row": 1, "selected_column": 2}
|
|
)
|
|
statements = _statements()
|
|
assert any(
|
|
"lv_table_set_selected_cell(table_sel_both->obj, 1, 2)" in s for s in statements
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_selected_cell_with_only_row_selects_whole_row(setup_core) -> None:
|
|
await _create_table({"id": "table_sel_row", "selected_row": 1})
|
|
statements = _statements()
|
|
assert any(
|
|
"lv_table_set_selected_cell(table_sel_row->obj, 1, LV_TABLE_CELL_NONE)" in s
|
|
for s in statements
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_selected_cell_omitted_entirely_when_not_configured(
|
|
setup_core,
|
|
) -> None:
|
|
await _create_table({"id": "table_no_selection", "rows": [["a"]]})
|
|
statements = _statements()
|
|
assert not any("lv_table_set_selected_cell" in s for s in statements)
|
|
|
|
|
|
@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())
|
|
|
|
entry = ACTION_REGISTRY["lvgl.table.cell.update"]
|
|
config = entry.schema(
|
|
{"id": "table_update", "row": 1, "column": 1, "text": "new value"}
|
|
)
|
|
action_id = ID("test_cell_update_action", is_declaration=True, type=entry.type_id)
|
|
await entry.coroutine_fun(config, action_id, TemplateArguments(), [])
|
|
|
|
statements = _statements()[before:]
|
|
assert any(
|
|
'lv_table_set_cell_value(table_update->obj, 1, 1, "new value")' in s
|
|
for s in statements
|
|
)
|
|
# Neither control flag was specified, so neither call should be emitted.
|
|
assert not any("LV_TABLE_CELL_CTRL" in s for s in statements)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_value_registers_a_value_changed_event_callback(setup_core) -> None:
|
|
config = container_schema(table_spec)(
|
|
{
|
|
"id": "table_on_value",
|
|
"rows": [["a"]],
|
|
"on_value": [
|
|
{"lambda": make_data_base("id(table_on_value).get_selected_row();")}
|
|
],
|
|
}
|
|
)
|
|
# 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_VALUE][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, table_spec, parent)
|
|
set_widgets_completed(True)
|
|
await generate_triggers()
|
|
|
|
statements = _statements()
|
|
assert any(
|
|
"table_on_value->obj" in s
|
|
and "add_event_cb" in s
|
|
and "LV_EVENT_VALUE_CHANGED" in s
|
|
for s in statements
|
|
)
|