diff --git a/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp b/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp index d5e9f366..2bb5060a 100644 --- a/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp @@ -124,6 +124,7 @@ NomadNetScreen::NomadNetScreen() { set_status("Enter a NomadNet address");show_start();hide(); } NomadNetScreen::~NomadNetScreen(){ + cancel_status_timer(); finish_field_edit(false); if(_field_editor)lv_textarea_set_text(_field_editor,""); if(_screen)lv_obj_del(_screen); @@ -382,11 +383,29 @@ void NomadNetScreen::set_address_editing(bool editing){ else{lv_group_add_obj(group,_edit_button);lv_group_focus_obj(_edit_button);} } } +void NomadNetScreen::cancel_status_timer(){ + if(!_status_timer)return; + lv_timer_del(_status_timer); + _status_timer=nullptr; +} +void NomadNetScreen::status_timer_cb(lv_timer_t* timer){ + auto* screen=static_cast(timer->user_data); + if(!screen||screen->_status_timer!=timer)return; + screen->_status_timer=nullptr; + screen->apply_browser_layout(false); +} void NomadNetScreen::set_status(const char* value){ + cancel_status_timer(); const char* status=value?value:""; lv_label_set_text(_status,status); const bool loaded_ack=_page_loaded&&std::strncmp(status,"Page loaded",11)==0; + const bool cached_ack=_page_loaded&&std::strncmp(status,"Cached page",11)==0; apply_browser_layout(!loaded_ack); + if(cached_ack){ + _status_timer=lv_timer_create(status_timer_cb,1500,this); + if(_status_timer)lv_timer_set_repeat_count(_status_timer,1); + else apply_browser_layout(false); + } } bool NomadNetScreen::set_page(const NomadNet::Document& document) { // Prepare every heap-backed model before touching the published page. The diff --git a/lib/tdeck_ui/UI/LXMF/NomadNetScreen.h b/lib/tdeck_ui/UI/LXMF/NomadNetScreen.h index 900b4828..66c2330d 100644 --- a/lib/tdeck_ui/UI/LXMF/NomadNetScreen.h +++ b/lib/tdeck_ui/UI/LXMF/NomadNetScreen.h @@ -124,6 +124,7 @@ private: lv_obj_t* _reload_button=nullptr; lv_obj_t* _save_button=nullptr; lv_obj_t* _identify_button=nullptr; lv_obj_t* _address_row=nullptr; lv_obj_t* _address=nullptr; lv_obj_t* _go_button=nullptr; lv_obj_t* _address_summary=nullptr; lv_obj_t* _edit_button=nullptr; lv_obj_t* _status=nullptr; lv_obj_t* _content=nullptr; lv_obj_t* _field_editor=nullptr; + lv_timer_t* _status_timer=nullptr; lv_obj_t* _directory=nullptr; NomadNet::CompactPage _page; NomadNet::FormState _form_state; @@ -159,6 +160,8 @@ private: Callback _back,_home; OpenCallback _reload,_open; LinkCallback _link; SubmitCallback _submit; SaveCallback _save; IdentifyCallback _identify; void set_address_editing(bool editing); + static void status_timer_cb(lv_timer_t* timer); + void cancel_status_timer(); void apply_browser_layout(bool show_status); void render_directory(View view); void show_browser(bool editing); diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index 73de175b..4d4380b7 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -2691,10 +2691,6 @@ void UIManager::nomad_update() { nomad_stop_transport(); break; } - const bool has_password = std::any_of(document.fields.begin(), document.fields.end(), - [](const NomadNet::FormField& field) { - return field.type == NomadNet::FormFieldType::PASSWORD; - }); const bool ordinary_nil = _nomad_request_data_class == NomadNet::RequestDataClass::NIL; const auto directive = NomadNet::parse_cache_directive(bytes.data(), bytes.size()); @@ -2704,7 +2700,7 @@ void UIManager::nomad_update() { const NomadNet::CacheKey cache_key{ _nomad_url.destination_hex, _nomad_url.path, _nomad_request_data_class}; - if (ordinary_nil && valid_document && !has_password && cache_now && + if (ordinary_nil && valid_document && cache_now && directive.valid && directive.ttl) { _nomad_cache_pending_key = cache_key; _nomad_cache_pending_body = _nomad_response.take(); @@ -2713,7 +2709,7 @@ void UIManager::nomad_update() { _nomad_cache_pending_generation = event.generation; } else { _nomad_response.release(); - if (ordinary_nil && (!directive.valid || directive.ttl == 0 || has_password)) + if (ordinary_nil && (!directive.valid || directive.ttl == 0)) { _nomad_cache_pending_key = cache_key; _nomad_cache_pending_generation = event.generation; _nomad_cache_pending_invalidate = true; } diff --git a/tests/native/nomadnet_lvgl_acceptance/CMakeLists.txt b/tests/native/nomadnet_lvgl_acceptance/CMakeLists.txt index 0d306d12..924d237f 100644 --- a/tests/native/nomadnet_lvgl_acceptance/CMakeLists.txt +++ b/tests/native/nomadnet_lvgl_acceptance/CMakeLists.txt @@ -34,5 +34,6 @@ target_include_directories(nomadnet_lvgl_acceptance PRIVATE target_compile_definitions(nomadnet_lvgl_acceptance PRIVATE ARDUINO=1) target_compile_options(nomadnet_lvgl_acceptance PRIVATE -Wall -Wextra -fsanitize=address,undefined -fno-omit-frame-pointer) -target_link_options(nomadnet_lvgl_acceptance PRIVATE -fsanitize=address,undefined) +target_link_options(nomadnet_lvgl_acceptance PRIVATE -fsanitize=address,undefined + -Wl,--wrap=lv_timer_create) target_link_libraries(nomadnet_lvgl_acceptance PRIVATE lvgl) diff --git a/tests/native/nomadnet_lvgl_acceptance/acceptance.cpp b/tests/native/nomadnet_lvgl_acceptance/acceptance.cpp index 3bb9f34a..e925a4b6 100644 --- a/tests/native/nomadnet_lvgl_acceptance/acceptance.cpp +++ b/tests/native/nomadnet_lvgl_acceptance/acceptance.cpp @@ -23,6 +23,17 @@ lv_group_t* group = nullptr; lv_disp_t* display = nullptr; lv_indev_t* keyboard = nullptr; std::vector framebuffer(320 * 240); +bool fail_next_timer_create = false; + +extern "C" lv_timer_t* __real_lv_timer_create(lv_timer_cb_t, uint32_t, void*); +extern "C" lv_timer_t* __wrap_lv_timer_create(lv_timer_cb_t callback, uint32_t period, + void* user_data) { + if (fail_next_timer_create) { + fail_next_timer_create = false; + return nullptr; + } + return __real_lv_timer_create(callback, period, user_data); +} struct KeyFeed { uint32_t key = 0; uint8_t phase = 0; } key_feed; @@ -187,7 +198,8 @@ int main() { bool focus_events = false, edge_scroll = false; bool ready = false, cancel = false, enter = false, escape = false, focus_restore = false; bool table_pixels = false, form_pixels = false, focus_pixels = false, glyph_pixels = false; - bool background_pixels = false, teardown = false; + bool background_pixels = false, teardown = false, cached_status_transient = false; + bool cached_status_oom_collapses = false; int delete_events = 0; UI::LXMF::NomadNet::DocumentParser parser; @@ -200,6 +212,24 @@ int main() { "---\n`[Next`:/page/next.mu]\n`[Submit`:/page/form.mu`username]"); assert(screen.set_page(fit_doc)); lv_obj_update_layout(screen._screen); + screen.set_status("Cached page; current reachability not checked"); + lv_obj_update_layout(screen._screen); + const int16_t content_with_cached_notice = lv_obj_get_height(screen._content); + const bool cached_notice_visible = + !lv_obj_has_flag(screen._status, LV_OBJ_FLAG_HIDDEN); + for (int i = 0; i < 12; ++i) pump(); + lv_obj_update_layout(screen._screen); + cached_status_transient = cached_notice_visible && + lv_obj_has_flag(screen._status, LV_OBJ_FLAG_HIDDEN) && + lv_obj_get_height(screen._content) > content_with_cached_notice; + screen.set_status("Checking page cache..."); + fail_next_timer_create = true; + screen.set_status("Cached page; current reachability not checked"); + lv_obj_update_layout(screen._screen); + cached_status_oom_collapses = !fail_next_timer_create && + screen._status_timer == nullptr && + lv_obj_has_flag(screen._status, LV_OBJ_FLAG_HIDDEN) && + lv_obj_get_height(screen._content) > content_with_cached_notice; const auto fit = screen._table_layout; fit_tier = fit.valid && fit.tier == UI::LXMF::NomadNet::TableLayoutTier::FIT; fit_columns = fit.columns == 2 && fit.cards == 0 && fit.x >= 0 && fit.width > 0 && @@ -338,15 +368,17 @@ int main() { "LVGL ACCEPT 320x240 fit_tier=%d fit_columns=%d reflow_tier=%d reflow_cards=%d " "stacked_tier=%d stacked_cards=%d stacked_pixels=%d stacked_scroll=%d stacked_objects=%d " "focus_events=%d edge_scroll=%d ready=%d cancel=%d enter=%d escape=%d focus_restore=%d " - "teardown=%d stale_group=%d background_pixels=%d table_pixels=%d form_pixels=%d " + "teardown=%d cached_status_transient=%d cached_status_oom_collapses=%d stale_group=%d background_pixels=%d table_pixels=%d form_pixels=%d " "focus_pixels=%d glyph_pixels=%d exact_fonts=1 objects=%u\n", fit_tier, fit_columns, reflow_tier, reflow_cards, stacked_tier, stacked_cards, stacked_pixels, stacked_scroll, stacked_objects, focus_events, edge_scroll, - ready, cancel, enter, escape, focus_restore, teardown, 0, background_pixels, + ready, cancel, enter, escape, focus_restore, teardown, cached_status_transient, + cached_status_oom_collapses, 0, background_pixels, table_pixels, form_pixels, focus_pixels, glyph_pixels, remaining); return fit_tier && fit_columns && reflow_tier && reflow_cards && stacked_tier && stacked_cards && stacked_pixels && stacked_scroll && stacked_objects && focus_events && edge_scroll && - ready && cancel && enter && escape && focus_restore && teardown && background_pixels && + ready && cancel && enter && escape && focus_restore && teardown && cached_status_transient && + cached_status_oom_collapses && background_pixels && table_pixels && form_pixels && focus_pixels && glyph_pixels && remaining == 0 ? 0 : 1; } diff --git a/tests/native/test_app_launcher_nomadnet.py b/tests/native/test_app_launcher_nomadnet.py index 4666d79a..d5c95f5e 100644 --- a/tests/native/test_app_launcher_nomadnet.py +++ b/tests/native/test_app_launcher_nomadnet.py @@ -1057,13 +1057,26 @@ def test_nomadnet_same_destination_navigation_reuses_active_link(): "nomad_finish_request_keep_link();" ) application_failure = response[response.index("if (!page_applied)"): - response.index("const bool has_password")] + response.index("const bool ordinary_nil")] assert "nomad_stop_transport();" in application_failure retained = response[response.index("_nomad_link.status() == Type::Link::ACTIVE"):] assert "_nomad_destination_hash.toHex() == _nomad_url.destination_hex" in retained assert "nomad_stop_transport();" in retained +def test_nomadnet_cache_persists_only_unsubmitted_server_response_bodies(): + manager_cpp = (INCLUDE / "UIManager.cpp").read_text() + response = manager_cpp[manager_cpp.index("case NomadNet::AsyncMailbox::Kind::RESPONSE:"): + manager_cpp.index("case NomadNet::AsyncMailbox::Kind::NONE:")] + + # A password descriptor in server markup does not contain mutable editor state. + # Cache admission is based on the request-data class, not field descriptors. + assert "has_password" not in response + assert "_nomad_request_data_class == NomadNet::RequestDataClass::NIL" in response + assert "ordinary_nil && valid_document && cache_now" in response + assert "_nomad_cache_pending_body = _nomad_response.take();" in response + + def test_nomadnet_owner_routes_back_reload_and_table_observation_is_private(): screen_h = (INCLUDE / "NomadNetScreen.h").read_text() manager_h = (INCLUDE / "UIManager.h").read_text() diff --git a/tests/native/test_nomadnet_lvgl_acceptance.py b/tests/native/test_nomadnet_lvgl_acceptance.py index 16ea3fd7..3661b4e3 100644 --- a/tests/native/test_nomadnet_lvgl_acceptance.py +++ b/tests/native/test_nomadnet_lvgl_acceptance.py @@ -41,7 +41,7 @@ def test_actual_nomadnet_screen_320x240_acceptance(tmp_path): assert "stacked_tier=1 stacked_cards=1 stacked_pixels=1 stacked_scroll=1 stacked_objects=1" in output assert "focus_events=1 edge_scroll=1" in output assert "ready=1 cancel=1 enter=1 escape=1 focus_restore=1" in output - assert "teardown=1 stale_group=0" in output + assert "teardown=1 cached_status_transient=1 cached_status_oom_collapses=1 stale_group=0" in output assert "background_pixels=1 table_pixels=1 form_pixels=1 focus_pixels=1 glyph_pixels=1" in output assert "exact_fonts=1" in output assert output.endswith("objects=0")