Files
trail-mate/modules/ui_shared/tests/geocaching/test_page.cpp
T

339 lines
16 KiB
C++

#include "ui/components/two_pane_styles.h"
#include "ui/page/page_profile.h"
#include "ui/screens/geocaching/geocaching_page_shell.h"
#include "ui/screens/gps/gps_page_runtime.h"
#include "ui/widgets/top_bar_power_presenter.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
namespace gps::ui::runtime
{
const shell::Host* test_map_host = nullptr;
MapTarget test_map_target;
bool enter_target(const shell::Host* host, lv_obj_t*, const MapTarget& target)
{
test_map_host = host;
test_map_target = target;
return true;
}
void exit(lv_obj_t*) { test_map_host = nullptr; }
} // namespace gps::ui::runtime
namespace ui::widgets::top_bar_power
{
void bind(TopBar& bar) { lv_label_set_text(bar.right_label, LV_SYMBOL_BATTERY_FULL " 100%"); }
void unbind(TopBar&) {}
} // namespace ui::widgets::top_bar_power
struct TestSource : ui::geocaching::Source
{
ui::geocaching::DraftInput draft;
std::string draft_name, draft_description, draft_hint;
bool has_draft = false, fail_save = false;
unsigned pending_reads = 0;
unsigned cancelled_reads = 0;
bool fail_read = false;
unsigned publications = 0;
uint8_t current_author = 7;
unsigned reads = 0, opens = 0, refreshes = 0, downloads = 0;
size_t saved_index = SIZE_MAX;
size_t last_open = 0;
uint64_t generation = 1;
void snapshot(ui::geocaching::Section section, ui::geocaching::Snapshot& out) override
{
out = {};
out.generation = generation;
out.count = 20;
out.can_refresh = true;
if (section == ui::geocaching::Section::Published)
{
out.count = has_draft ? 1 : 0;
out.can_create = true;
}
std::snprintf(out.status.data(), out.status.size(), "20 shared caches");
}
bool item(ui::geocaching::Section section, size_t index, uint64_t expected, ui::geocaching::Item& out) override
{
++reads;
if (index >= 20 || expected != generation) return false;
out = {};
if (section == ui::geocaching::Section::Published)
{
if (!has_draft || index) return false;
out.is_draft = true;
out.edit_generation = draft.generation;
std::memcpy(out.id.data(), draft.id.data(), 16);
std::snprintf(out.name.data(), out.name.size(), "%s", draft_name.c_str());
return true;
}
out.id[0] = static_cast<uint8_t>(index);
out.downloaded = saved_index == index;
out.can_download = !out.downloaded;
std::snprintf(out.name.data(), out.name.size(), "Cache %02u - woodland trail", unsigned(index + 1));
std::snprintf(out.detail.data(), out.detail.size(), "30.5000000, 120.5000000\nDifficulty 2.0 / Terrain 2.5\nDirectory preview - not yet downloaded");
return true;
}
void refresh(ui::geocaching::Section) override
{
++refreshes;
++generation;
}
void open(const ui::geocaching::Item& item, uint64_t expected) override
{
if (expected == generation)
{
++opens;
last_open = item.id[0];
}
}
bool download(const ui::geocaching::Item& item, uint64_t expected) override
{
if (expected != generation) return false;
++downloads;
saved_index = item.id[0];
++generation;
return true;
}
bool saveDraft(ui::geocaching::DraftInput& input) override
{
if (fail_save) return true;
input.id.fill(0x42);
draft = input;
++draft.generation;
draft_name = input.name;
draft_description = input.description;
draft_hint = input.hint;
draft.name = draft_name;
draft.description = draft_description;
draft.hint = draft_hint;
has_draft = true;
++generation;
return true;
}
ui::geocaching::DraftSaveStatus draftSaveStatus(const std::array<uint8_t, 16>&, uint64_t) override
{
return fail_save ? ui::geocaching::DraftSaveStatus::Failed : ui::geocaching::DraftSaveStatus::Saved;
}
ui::geocaching::DraftReadStatus readDraft(const std::array<uint8_t, 16>& id, void (*sink)(const ui::geocaching::DraftInput&, void*), void* context) override
{
if (pending_reads)
{
--pending_reads;
return ui::geocaching::DraftReadStatus::Pending;
}
if (fail_read) return ui::geocaching::DraftReadStatus::Failed;
if (!has_draft || id != draft.id) return ui::geocaching::DraftReadStatus::Failed;
sink(draft, context);
return ui::geocaching::DraftReadStatus::Ready;
}
void cancelDraftRead(const std::array<uint8_t, 16>& id) override
{
if (id == draft.id) ++cancelled_reads;
}
bool publicationAuthor(const std::array<uint8_t, 16>& id, uint64_t version, std::array<uint8_t, 64>& author, uint32_t* from, uint32_t* to) override
{
if (!has_draft || id != draft.id || version != draft.generation) return false;
author.fill(current_author);
if (from) *from = 0;
if (to) *to = 1;
return true;
}
bool publishDraft(const std::array<uint8_t, 16>& id, uint64_t version, const std::array<uint8_t, 64>& expected, uint32_t revision) override
{
std::array<uint8_t, 64> current;
if (!publicationAuthor(id, version, current, nullptr, nullptr) || current != expected || revision != 1) return false;
++publications;
return true;
}
};
bool save(const std::string& path, lv_obj_t* screen)
{
lv_obj_update_layout(screen);
auto* image = lv_snapshot_take(screen, LV_COLOR_FORMAT_RGB888);
if (!image) return false;
std::ofstream out(path, std::ios::binary);
out << "P6\n"
<< image->header.w << " " << image->header.h << "\n255\n";
for (unsigned y = 0; y < image->header.h; ++y)
for (unsigned x = 0; x < image->header.w; ++x)
{
const auto* pixel = image->data + y * image->header.stride + x * 3;
const char rgb[] = {char(pixel[2]), char(pixel[1]), char(pixel[0])};
out.write(rgb, 3);
}
lv_draw_buf_destroy(image);
return out.good();
}
int main(int argc, char** argv)
{
if (argc != 4) return 1;
const int width = std::atoi(argv[1]), height = std::atoi(argv[2]);
lv_init();
auto* display = lv_display_create(width, height);
std::vector<uint8_t> pixels(width * 32 * 4);
lv_display_set_color_format(display, LV_COLOR_FORMAT_XRGB8888);
lv_display_set_buffers(display, pixels.data(), nullptr, pixels.size(), LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_display_set_flush_cb(display, [](lv_display_t* display, const lv_area_t*, uint8_t*)
{ lv_display_flush_ready(display); });
const auto profile = width == 480 ? ui::page_profile::make_pager_profile() : ui::page_profile::make_tdeck_profile();
ui::page_profile::set_active_profile(&profile);
auto* screen = lv_screen_active();
auto* old_group = lv_group_create();
lv_group_set_default(old_group);
unsigned exits = 0;
ui::page::Host host{&exits, [](void* context)
{ ++*static_cast<unsigned*>(context); }};
geocaching::ui::shell::enter(&host, screen);
auto* root = lv_obj_get_child(screen, 0);
if (!root || !save(std::string(argv[3]) + "-empty.ppm", screen)) return 2;
auto* tabs = lv_obj_get_child(root, 1);
for (unsigned i = 0; i < 3; ++i)
{
auto* tab = lv_obj_get_child(tabs, i);
if (lv_color_to_u32(lv_obj_get_style_bg_color(tab, LV_PART_MAIN)) == lv_color_to_u32(lv_palette_main(LV_PALETTE_BLUE))) return 3;
}
TestSource source;
geocaching::ui::shell::bind(&source);
lv_tick_inc(501);
lv_timer_handler();
auto* list = lv_obj_get_child(root, 3);
const auto visible = lv_obj_get_child_count(list);
if (visible < 2 || visible > 4 || source.reads != visible) return 4;
if (!save(std::string(argv[3]) + "-list.ppm", screen)) return 5;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
if (source.opens != 1 || source.last_open != 0) return 6;
if (!save(std::string(argv[3]) + "-detail.ppm", screen)) return 11;
auto* details_footer = lv_obj_get_child(root, 4);
lv_obj_send_event(lv_obj_get_child(details_footer, 3), LV_EVENT_CLICKED, nullptr);
if (source.downloads != 1 || source.saved_index != 0 ||
lv_obj_has_state(lv_obj_get_child(details_footer, 3), LV_STATE_DISABLED)) return 14;
lv_obj_send_event(lv_obj_get_child(details_footer, 3), LV_EVENT_CLICKED, nullptr);
if (!gps::ui::runtime::test_map_host || !lv_obj_has_flag(root, LV_OBJ_FLAG_HIDDEN) ||
std::strcmp(gps::ui::runtime::test_map_target.name, "Cache 01 - woodland trail")) return 15;
source.saved_index = 1;
++source.generation;
ui::map::MapOverlaySnapshot overlays;
const auto& map_target = gps::ui::runtime::test_map_target;
if (!map_target.append_overlays) return 17;
map_target.append_overlays(map_target.overlay_context, overlays);
if (overlays.item_count != 1 || overlays.items[0].kind != ui::map::MapOverlayKind::Geocache) return 18;
overlays.item_count = ui::map::MapOverlaySnapshot::kMaxItems;
map_target.append_overlays(map_target.overlay_context, overlays);
if (!overlays.truncated || overlays.item_count != ui::map::MapOverlaySnapshot::kMaxItems) return 19;
if (!map_target.select_overlay || overlays.items[0].stable_id != 2) return 20;
map_target.select_overlay(map_target.overlay_context, overlays.items[0].stable_id);
ui::page::request_exit(gps::ui::runtime::test_map_host);
ui::page::request_exit(gps::ui::runtime::test_map_host);
lv_tick_inc(10);
lv_timer_handler();
if (gps::ui::runtime::test_map_host || lv_obj_has_flag(root, LV_OBJ_FLAG_HIDDEN) || exits) return 16;
if (std::strcmp(lv_label_get_text(lv_obj_get_child(list, 0)), "Cache 02 - woodland trail")) return 21;
lv_obj_send_event(lv_obj_get_child(lv_obj_get_child(root, 0), 0), LV_EVENT_CLICKED, nullptr);
if (exits || lv_obj_get_child_count(list) != visible) return 12;
auto* footer = lv_obj_get_child(root, 4);
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
if (source.opens != 2 || source.last_open != visible) return 7;
lv_obj_send_event(lv_obj_get_child(lv_obj_get_child(root, 0), 0), LV_EVENT_CLICKED, nullptr);
if (exits) return 13;
lv_obj_send_event(lv_obj_get_child(lv_obj_get_child(root, 0), 0), LV_EVENT_CLICKED, nullptr);
if (exits != 1) return 8;
geocaching::ui::shell::exit(nullptr, screen);
if (lv_obj_get_child_count(screen) || lv_group_get_default() != old_group) return 9;
geocaching::ui::shell::enter(&host, screen);
root = lv_obj_get_child(screen, 0);
tabs = lv_obj_get_child(root, 1);
lv_obj_send_event(lv_obj_get_child(tabs, 2), LV_EVENT_CLICKED, nullptr);
footer = lv_obj_get_child(root, 4);
lv_obj_send_event(lv_obj_get_child(footer, 0), LV_EVENT_CLICKED, nullptr);
list = lv_obj_get_child(root, 3);
auto* name_field = lv_obj_get_child(list, 1);
lv_textarea_set_text(name_field, "林间宝藏");
lv_textarea_set_text(lv_obj_get_child(list, 3), "30.5");
lv_textarea_set_text(lv_obj_get_child(list, 5), "120.5");
const std::string description = std::string(63, 'a') + "宝藏";
lv_textarea_set_text(lv_obj_get_child(list, 7), description.c_str());
if (!save(std::string(argv[3]) + "-editor.ppm", screen)) return 27;
source.fail_save = true;
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_has_state(name_field, LV_STATE_DISABLED) || std::strcmp(lv_textarea_get_text(name_field), "林间宝藏")) return 22;
source.fail_save = false;
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
lv_tick_inc(600);
lv_timer_handler();
if (!source.has_draft || source.draft.latitude_e7 != 305000000 || source.draft.longitude_e7 != 1205000000) return 23;
source.pending_reads = 1;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
if (!lv_obj_has_state(lv_obj_get_child(list, 1), LV_STATE_DISABLED) ||
!lv_obj_has_state(lv_obj_get_child(footer, 3), LV_STATE_DISABLED)) return 33;
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_has_state(lv_obj_get_child(list, 1), LV_STATE_DISABLED)) return 34;
if (std::strcmp(lv_textarea_get_text(lv_obj_get_child(list, 1)), "林间宝藏") ||
description != lv_textarea_get_text(lv_obj_get_child(list, 7))) return 24;
lv_textarea_set_text(lv_obj_get_child(list, 1), "Discard this");
lv_obj_send_event(lv_obj_get_child(footer, 0), LV_EVENT_CLICKED, nullptr);
if (lv_obj_has_flag(lv_obj_get_child(footer, 2), LV_OBJ_FLAG_HIDDEN)) return 25;
lv_obj_send_event(lv_obj_get_child(footer, 2), LV_EVENT_CLICKED, nullptr);
if (source.draft_name != "林间宝藏") return 26;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
lv_tick_inc(600);
lv_timer_handler();
lv_obj_send_event(lv_obj_get_child(footer, 2), LV_EVENT_CLICKED, nullptr);
if (!std::strstr(lv_label_get_text(lv_obj_get_child(list, 0)), "publicly") || source.publications) return 28;
if (!save(std::string(argv[3]) + "-publish.ppm", screen)) return 29;
lv_obj_send_event(lv_obj_get_child(footer, 0), LV_EVENT_CLICKED, nullptr);
if (source.publications || lv_obj_has_state(lv_obj_get_child(list, 1), LV_STATE_DISABLED)) return 30;
lv_obj_send_event(lv_obj_get_child(footer, 2), LV_EVENT_CLICKED, nullptr);
++source.current_author;
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
if (source.publications || !std::strstr(lv_label_get_text(lv_obj_get_child(root, 2)), "not started")) return 31;
lv_obj_send_event(lv_obj_get_child(footer, 0), LV_EVENT_CLICKED, nullptr);
lv_obj_send_event(lv_obj_get_child(footer, 2), LV_EVENT_CLICKED, nullptr);
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
if (source.publications != 1 || lv_obj_get_child_count(list) != 1) return 32;
source.pending_reads = 2;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
lv_obj_send_event(lv_obj_get_child(footer, 0), LV_EVENT_CLICKED, nullptr);
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_get_child_count(list) != 1 || source.pending_reads != 1 || source.cancelled_reads != 1) return 35;
source.pending_reads = 0;
source.fail_read = true;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
if (lv_obj_get_child_count(list) != 1 || !std::strstr(lv_label_get_text(lv_obj_get_child(root, 2)), "Cannot read")) return 36;
source.fail_read = false;
source.pending_reads = 2;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
geocaching::ui::shell::bind(nullptr);
if (source.cancelled_reads != 2 || lv_obj_get_child_count(list) != 1) return 37;
geocaching::ui::shell::bind(&source);
source.pending_reads = 2;
lv_tick_inc(600);
lv_timer_handler();
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
geocaching::ui::shell::exit(nullptr, screen);
if (source.cancelled_reads != 3) return 38;
// Warm LVGL style caches before checking repeated page lifetime cleanup.
geocaching::ui::shell::enter(&host, screen);
geocaching::ui::shell::exit(nullptr, screen);
lv_mem_monitor_t baseline{}, after{};
lv_mem_monitor(&baseline);
for (unsigned i = 0; i < 20; ++i)
{
geocaching::ui::shell::enter(&host, screen);
geocaching::ui::shell::exit(nullptr, screen);
}
lv_mem_monitor(&after);
if (after.free_size != baseline.free_size || lv_obj_get_child_count(screen)) return 10;
geocaching::ui::shell::bind(nullptr);
lv_group_delete(old_group);
std::printf("%dx%d: visible=%u, no LVGL growth after 20 enter/exit cycles\n", width, height, unsigned(visible));
return 0;
}