Files
homeassistant_espscreen/tests/test_settings_screen.cpp
T
Max Gramser ffe639e23c Let the page buttons make way: a Page buttons setting that gives their room to the tiles, chevrons and dots instead of words, and a warning in the editor for pages nothing leads to (0.2.82)
GitHub issue #9 asked to drop the Previous/Next bar when Go to page tiles do the navigation and to give the tiles the whole screen. Page buttons is a setting the screen owns (on by default): a row in the Screen group of the settings page, switch.<screen>_page_buttons in Home Assistant, and a row in the editor's Screen settings. Without the bar, and on a screen with one page, place_page moves and sizes the three rows so they reach the bottom margin (Guition 108 -> 122 px, CYD 52 -> 61 px); a card keeps its circle, name and state in its middle and the single analog clock keeps its dial. With page buttons and swiping both off, the editor says where the Go to page tiles lead and which pages that leaves out of reach.

The bar itself: each half of the band under the tiles is one key with an MDI chevron and lights up under a finger, and a dot per page sits between them; the screen's settings page uses the same pager. App 0.2.82, firmware 0.2.69.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:43:43 +02:00

201 lines
11 KiB
C++

#define SETTINGS_SCREEN_TEST
#include "../components/smart_display/settings_screen.h"
#include <cassert>
#include <string>
using namespace settings_screen;
// The row of a page by its label, so a test says what it means.
static const Row &row_named(const Page &page, const std::string &label) {
for (uint8_t i = 0; i < page.count; ++i)
if (label == page.rows[i].label) return page.rows[i];
assert(false);
return page.rows[0];
}
int main() {
// ---- the duration ladder: seconds low down, quarters of an hour up top, and symmetric ----
assert(ladder_step(60) == 30 && ladder_step(600) == 60 && ladder_step(1800) == 300);
assert(ladder_step(3600) == 900 && ladder_step(36000) == 1800);
Row timeout = duration("After", nullptr, nullptr, 30, 3600);
assert(stepped(timeout, 120, 1) == 150 && stepped(timeout, 120, -1) == 90);
assert(stepped(timeout, 300, 1) == 360 && stepped(timeout, 360, -1) == 300);
assert(stepped(timeout, 300, -1) == 270 && stepped(timeout, 270, 1) == 300);
assert(stepped(timeout, 30, -1) == 30 && stepped(timeout, 3600, 1) == 3600);
assert(at_end(timeout, 30, -1) && at_end(timeout, 3600, 1) && !at_end(timeout, 120, -1));
// ---- a number stops at its ends, a moment walks around midnight ----
Row percent = number("Brightness", nullptr, nullptr, 5, 100, 5, "%");
assert(stepped(percent, 100, 1) == 100 && stepped(percent, 5, -1) == 5 && stepped(percent, 95, 1) == 100);
Row start = moment("Starts", nullptr, nullptr);
assert(stepped(start, 1320, 1) == 1335 && stepped(start, 1425, 1) == 0 && stepped(start, 0, -1) == 1425);
assert(!at_end(start, 0, -1));
// Holding the key walks whole hours, and first lands on the hour it is in.
assert(stepped(start, 1320, 1, true) == 1380 && stepped(start, 1320, -1, true) == 1260);
assert(stepped(start, 1327, 1, true) == 1380 && stepped(start, 1327, -1, true) == 1320);
assert(stepped(start, 1380, 1, true) == 1440 % 1440); // 23:00 + 1 h wraps to 00:00
assert(stepped(start, 0, -1, true) == 1380); // 00:00 - 1 h is 23:00
// ---- texts ----
assert(duration_text(45) == "45 sec" && duration_text(60) == "1 min" && duration_text(120) == "2 min");
assert(duration_text(3600) == "1 h" && duration_text(5400) == "1 h 30" && duration_text(86400) == "24 h");
assert(moment_text(1320, true) == "22:00" && moment_text(0, true) == "00:00");
assert(moment_text(1320, false) == "10:00 PM" && moment_text(0, false) == "12:00 AM");
assert(moment_text(750, false) == "12:30 PM" && moment_text(60, false) == "1:00 AM");
// ---- a switch's knob: inset at the left when off, flush with the same inset at the right when on ----
// The Guition switch is 62 by 34 (a 28 px knob), the CYD one 40 by 22 (an 18 px knob).
assert(knob_x(62, 34, false) == 3 && knob_x(62, 34, true) == 31 && 31 + 28 + 3 == 62);
assert(knob_x(40, 22, false) == 2 && knob_x(40, 22, true) == 20 && 20 + 18 + 2 == 40);
// ---- rows read and write the real settings ----
screen_settings::current = screen_settings::Settings{};
const Page &light = pages[1];
const Row &brightness = row_named(light, "Brightness");
assert(value_text(brightness) == "100%");
brightness.write(40);
assert(screen_settings::current.brightness == 40 && value_text(brightness) == "40%");
// Lowering the normal brightness pulls the two dim levels down with it, exactly like the add-on.
screen_settings::current.standby_brightness = 60;
brightness.write(30);
assert(screen_settings::current.standby_brightness == 30);
const Row &standby = row_named(light, "Standby brightness");
standby.write(90);
assert(screen_settings::current.standby_brightness == 30); // never above the normal brightness
assert(screen_settings::current.valid());
// Dark mode (firmware 0.2.54+) sits right under Brightness, a switch of its own outside the frozen block.
assert(light.count == 5 && light.rows[1].kind == Kind::toggle && std::string(light.rows[1].label) == "Dark mode");
const Row &dark = row_named(light, "Dark mode");
assert(value_text(dark) == "Off");
dark.write(1);
assert(dark_mode == 1 && value_text(dark) == "On");
dark.write(0);
assert(dark_mode == 0 && value_text(dark) == "Off");
const Page &screen = pages[3];
// Page buttons (firmware 0.2.69+) follows Swipe between pages: on by default, off takes the bar away.
{
const Row &buttons = row_named(screen, "Page buttons");
assert(buttons.kind == Kind::toggle && &buttons == &row_named(screen, "Swipe between pages") + 1);
assert(page_buttons == 1 && value_text(buttons) == "On");
buttons.write(0);
assert(page_buttons == 0 && value_text(buttons) == "Off");
assert(set("page_buttons", 0) == SetResult::same);
buttons.write(1);
assert(page_buttons == 1);
}
const Row &clock = row_named(screen, "Clock");
assert(clock.option_count == 2 && value_text(clock) == "24 hour");
clock.write(0);
assert(screen_settings::current.clock_24h == 0 && value_text(clock) == "12 hour");
const Row &home = row_named(screen, "Back to page 1");
home.write(0);
assert(auto_home == 0 && value_text(home) == "Off");
const Row &rotate = row_named(screen, "Rotation");
rotation_supported = false;
assert(!visible_row(rotate)); // a board that cannot turn never shows the row
rotation_supported = true;
assert(visible_row(rotate));
rotate.write(2);
assert(rotation == 180 && value_text(rotate) == "180°");
rotate.write(3);
assert(rotation == 270);
rotate.write(0);
assert(rotation == 0);
// ---- every page hangs together: menu rows open a real page, controls can be worked ----
assert(PAGE_COUNT == 5);
for (uint8_t p = 0; p < PAGE_COUNT; ++p) {
const Page &page = pages[p];
assert(page.title && page.count && page.count <= 8);
for (uint8_t i = 0; i < page.count; ++i) {
const Row &row = page.rows[i];
assert(row.label && *row.label);
if (row.kind == Kind::page) assert(row.opens > 0 && row.opens < PAGE_COUNT && *row.icon);
if (row.kind == Kind::toggle || row.kind == Kind::choice || row.kind == Kind::number ||
row.kind == Kind::duration || row.kind == Kind::moment)
assert(row.read && row.write);
if (row.kind == Kind::number) assert(row.step > 0 && row.high > row.low);
if (row.kind == Kind::duration) assert(row.high > row.low);
if (row.kind == Kind::choice) assert(row.options && row.option_count);
if (row.kind == Kind::info) assert(row.text);
if (row.kind == Kind::action) assert(row.run && *row.icon);
}
}
// Every page is reachable from the menu.
bool reached[PAGE_COUNT] = {true};
for (uint8_t i = 0; i < pages[0].count; ++i) reached[pages[0].rows[i].opens] = true;
for (bool page : reached) assert(page);
// ---- how many rows fit, and when the pager appears ----
bool paged = false;
assert(fitting_rows(180, 34, 4, 26, 4, paged) == 4 && !paged); // CYD: four rows, no pager
assert(fitting_rows(180, 34, 4, 26, 6, paged) == 4 && paged); // six rows: 4 + 2
assert(fitting_rows(372, 52, 8, 40, 6, paged) == 6 && !paged); // Guition: the whole group at once
assert(fitting_rows(372, 52, 8, 40, 9, paged) == 5 && paged);
assert(fitting_rows(40, 34, 4, 26, 5, paged) >= 1); // never zero rows
// ---- a change is stored, applied and reported once ----
static int stored = 0, applied = 0;
static std::string last_key;
static int32_t last_value = 0;
store = [] { ++stored; };
apply = [] { ++applied; };
report = [](const char *key, int32_t value) { last_key = key; last_value = value; };
row_named(light, "Auto standby").write(0);
assert(stored == 1 && applied == 1 && last_key == "standby_enabled" && last_value == 0);
row_named(screen, "After").write(600);
assert(stored == 2 && last_key == "auto_home_seconds" && last_value == 600 && auto_home_seconds == 600);
// ---- set(): the one path for the rows and for Home Assistant's entities (firmware 0.2.49+) ----
// A value the screen already has is not stored, applied or reported: an automation may set it on every change.
SetResult result = set("standby_enabled", 0);
assert(result == SetResult::same && stored == 2 && applied == 2);
result = set("auto_home_seconds", 600);
assert(result == SetResult::same && stored == 2);
// Every key ESP Screens knows lands on the screen, clamped the way the page steps it.
screen_settings::current = screen_settings::Settings{};
rotation_supported = true;
const struct { const char *key; int32_t value, expected; } cases[] = {
{"standby_enabled", 5, 1}, {"standby_seconds", 10, 60}, {"standby_seconds", 999999, 86400},
{"brightness", 1, 5}, {"brightness", 250, 100}, {"standby_brightness", 101, 100}, {"night_enabled", 0, 0},
{"night_start", -5, 0}, {"night_end", 2000, 1439}, {"night_brightness", 7, 7}, {"clock_24h", 0, 0},
{"home_on_standby", 1, 1}, {"swipe_pages", 2, 1}, {"rotation", 100, 90}, {"rotation", 400, 270},
{"auto_home", 0, 0}, {"auto_home_seconds", 5, 30}, {"auto_home_seconds", 99999, 3600},
{"dark_mode", 7, 1}, {"page_buttons", 0, 0},
};
for (const auto &c : cases) {
result = set(c.key, c.value);
assert(result != SetResult::unknown);
if (result == SetResult::changed) assert(last_key == c.key && last_value == c.expected);
}
assert(screen_settings::current.standby_seconds == 86400 && screen_settings::current.brightness == 100);
assert(screen_settings::current.night_start == 0 && screen_settings::current.night_end == 1439);
assert(swipe_pages == 1 && rotation == 270 && auto_home == 0 && auto_home_seconds == 3600 && dark_mode == 1 && page_buttons == 0);
assert(screen_settings::current.valid());
// Dark mode again is the same look: nothing stored, applied or reported; off is a change.
{
const int stored_before = stored, applied_before = applied;
assert(set("dark_mode", 1) == SetResult::same && stored == stored_before && applied == applied_before);
assert(set("dark_mode", 0) == SetResult::changed && last_key == "dark_mode" && last_value == 0 && dark_mode == 0);
set("dark_mode", 1);
}
// A lower brightness pulls both dim levels down, and reports the brightness itself.
set("standby_brightness", 60);
set("night_brightness", 50);
result = set("brightness", 30);
assert(result == SetResult::changed && last_key == "brightness" && last_value == 30);
assert(screen_settings::current.standby_brightness == 30 && screen_settings::current.night_brightness == 30);
result = set("standby_brightness", 90);
assert(result == SetResult::same && screen_settings::current.standby_brightness == 30);
// What the screen cannot do, or does not know, is refused without a store.
const int before = stored;
rotation_supported = false;
result = set("rotation", 90);
assert(result == SetResult::unknown && rotation == 270);
result = set("show_clock", 0);
assert(result == SetResult::unknown);
result = set("beep", 1);
assert(result == SetResult::unknown && stored == before);
}