WiFiManager: fragment templates, handler refactor, and tests.

Split reusable HTML into Fragments, Message, and Param headers; route
more pages through DFTE streaming. Update handlers, WiFi polling JS,
and configuration/scanning tests; add template rendering coverage.
This commit is contained in:
2026-04-18 00:23:57 +10:00
parent 69aea5ce35
commit 9401c20df6
20 changed files with 1988 additions and 1044 deletions
+14
View File
@@ -108,6 +108,20 @@ TestCase tests[] = {
TEST_ENTRY(test_async_scan_behavior),
TEST_ENTRY(test_scan_status_checking),
TEST_ENTRY(test_scan_completion_wait),
TEST_ENTRY(test_scan_cancels_when_connect_pending),
TEST_ENTRY(test_scan_cancels_when_lifecycle_blocked),
TEST_ENTRY(test_scan_generation_invalidated_on_reset),
// Template rendering tests
TEST_ENTRY(test_page_shell_supports_page_assets),
TEST_ENTRY(test_root_template_renders_core_placeholders),
TEST_ENTRY(test_wifi_template_renders_scan_container_and_form_fields),
TEST_ENTRY(test_message_template_renders_body_and_actions),
TEST_ENTRY(test_param_template_renders_shared_actions),
TEST_ENTRY(test_info_template_renders_sections_and_footer),
TEST_ENTRY(test_fragment_action_form_renders_shared_button_markup),
TEST_ENTRY(test_fragment_info_row_and_section_render),
TEST_ENTRY(test_fragment_centered_button_supports_extra_attrs),
// State transition tests
TEST_ENTRY(test_portal_to_connected_transition),
+14
View File
@@ -118,6 +118,20 @@ void test_wifi_scan_initiates();
void test_async_scan_behavior();
void test_scan_status_checking();
void test_scan_completion_wait();
void test_scan_cancels_when_connect_pending();
void test_scan_cancels_when_lifecycle_blocked();
void test_scan_generation_invalidated_on_reset();
// Template rendering tests
void test_page_shell_supports_page_assets();
void test_root_template_renders_core_placeholders();
void test_wifi_template_renders_scan_container_and_form_fields();
void test_message_template_renders_body_and_actions();
void test_param_template_renders_shared_actions();
void test_info_template_renders_sections_and_footer();
void test_fragment_action_form_renders_shared_button_markup();
void test_fragment_info_row_and_section_render();
void test_fragment_centered_button_supports_extra_attrs();
// State transition tests
void test_portal_to_connected_transition();
@@ -65,17 +65,18 @@ void test_set_and_get_hostname() {
WiFiManager wm;
// Test with char*
wm.setHostname("test-hostname");
String hostname1 = wm.getWiFiHostname();
// Verify hostname was set (may be empty initially, but getter doesn't crash)
TEST_ASSERT_TRUE_MESSAGE(true, "setHostname(char*) and getWiFiHostname() executed");
// Test with String
wm.setHostname(String("test-hostname-2"));
String hostname2 = wm.getWiFiHostname();
// Verify getter works
TEST_ASSERT_TRUE_MESSAGE(true, "setHostname(String) and getWiFiHostname() executed");
TEST_ASSERT_TRUE_MESSAGE(wm.setHostname("test-hostname"), "Valid char* hostname should be accepted");
TEST_ASSERT_TRUE_MESSAGE(wm.setHostname(String(" trimmed-hostname ")), "Valid String hostname should be trimmed and accepted");
TEST_ASSERT_TRUE_MESSAGE(wm.setHostname(""), "Empty hostname should clear the configured hostname");
String tooLongHostname = "123456789012345678901234567890123";
TEST_ASSERT_FALSE_MESSAGE(wm.setHostname(tooLongHostname), "Hostnames longer than 32 chars should be rejected");
TEST_ASSERT_FALSE_MESSAGE(wm.setHostname("bad host"), "Hostnames with spaces should be rejected");
TEST_ASSERT_FALSE_MESSAGE(wm.setHostname("-leading"), "Hostnames cannot start with a hyphen");
TEST_ASSERT_FALSE_MESSAGE(wm.setHostname("trailing-"), "Hostnames cannot end with a hyphen");
(void)wm.getWiFiHostname();
TEST_ASSERT_TRUE_MESSAGE(true, "getWiFiHostname() executed after hostname validation checks");
Serial.println("[TEST] setHostname() and getWiFiHostname() test completed successfully");
}
@@ -0,0 +1,251 @@
#include <unity.h>
#include <Arduino.h>
#include <TemplateEngine.h>
#include "templates/Fragments.h"
#include "templates/Info.h"
#include "templates/Param.h"
#include "templates/PageShell.h"
#include "templates/Root.h"
#include "templates/WiFi.h"
#include "templates/Message.h"
#include "../test_main.h"
namespace {
String renderTemplate(const char* templateData, PlaceholderRegistry& registry) {
TemplateContext context;
context.setRegistry(&registry);
TemplateRenderer::initializeContext(context, templateData);
uint8_t buffer[128];
String output;
while (true) {
size_t written = TemplateRenderer::renderNextChunk(context, buffer, sizeof(buffer));
if (written == 0) {
break;
}
output.concat(reinterpret_cast<const char*>(buffer), written);
}
return output;
}
const char kEmpty[] PROGMEM = "";
const char kDocTitle[] PROGMEM = "Config ESP";
const char kBodyClass[] PROGMEM = "wifi";
const char kPageScripts[] PROGMEM = "<script>console.log('wifi page');</script>";
const char kPageStyles[] PROGMEM = "<style>.wifi{color:#000;}</style>";
const char kPageContent[] PROGMEM = "<div>Body</div>";
const char kScanContent[] PROGMEM = "Scanning for networks...<br/><br/>";
const char kSsidPlaceholder[] PROGMEM = "Office";
const char kPasswordPlaceholder[] PROGMEM = "********";
const char kStaticFields[] PROGMEM = "<hr><br/><label for='ip'>Static IP</label>";
const char kParamSection[] PROGMEM = "<hr><br/><label for='param_0'>Token</label>";
const char kFormActions[] PROGMEM = "<br/><br/><button type='submit'>Save</button>";
const char kPageActions[] PROGMEM = "<br/><div class='c'><button id='refresh-btn' type='button' onclick='return refreshScan()'>Refresh</button></div><hr><br/><form action='/' method='get'><button>Back</button></form>";
const char kMessageActions[] PROGMEM = "<hr><br/><form action='/' method='get'><button>Back</button></form>";
const char kStatus[] PROGMEM = "<div class='msg'>Status</div>";
const char kMessageBody[] PROGMEM = "<div class='msg S'>Saved</div>";
const char kInfoSection[] PROGMEM = "<h3>About</h3><hr><dl><dt>Build date</dt><dd>today</dd></dl>";
const char kInfoFooter[] PROGMEM = "<hr><br/><form action='/update' method='get'><button>Update</button></form>";
const char kPageTitle[] PROGMEM = "WiFiManager";
const char kSubtitle[] PROGMEM = "Setup";
const char kMenu[] PROGMEM = "<form><button>Configure WiFi</button></form>";
const char kActionPrefix[] PROGMEM = "<hr><br/>";
const char kActionPath[] PROGMEM = "/update";
const char kActionMethod[] PROGMEM = "get";
const char kActionLabel[] PROGMEM = "Update";
const char kActionClassAttr[] PROGMEM = " class='D'";
const char kActionSuffix[] PROGMEM = "<br/>\n";
const char kButtonId[] PROGMEM = "refresh-btn";
const char kButtonType[] PROGMEM = "button";
const char kButtonOnClick[] PROGMEM = "return refreshScan()";
const char kButtonExtraAttrs[] PROGMEM = " data-skip-initial-scan='true'";
const char kInfoLabel[] PROGMEM = "SDK version";
const char kInfoValue[] PROGMEM = "3.1.2";
const char kSectionTitle[] PROGMEM = "Device";
const char kSectionRows[] PROGMEM = "<dt>Chip ID</dt><dd>abc</dd>";
} // namespace
void test_page_shell_supports_page_assets() {
Serial.println("[TEST] Testing page shell page-scoped assets...");
PlaceholderRegistry registry(12);
registry.registerProgmemData("%SCRIPTS%", kEmpty);
registry.registerProgmemData("%PAGE_SCRIPTS%", kPageScripts);
registry.registerProgmemData("%STYLES%", kEmpty);
registry.registerProgmemData("%PAGE_STYLES%", kPageStyles);
registry.registerProgmemData("%DOC_TITLE%", kDocTitle);
registry.registerProgmemData("%BODY_CLASS%", kBodyClass);
registry.registerProgmemData("%PAGE_CONTENT%", kPageContent);
String output = renderTemplate(WM_PAGE_SHELL_TEMPLATE, registry);
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("wifi page"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(".wifi{color:#000;}"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<div>Body</div>"));
Serial.println("[TEST] Page shell page-scoped assets test completed successfully");
}
void test_wifi_template_renders_scan_container_and_form_fields() {
Serial.println("[TEST] Testing WiFi page template rendering...");
PlaceholderRegistry registry(16);
registry.registerProgmemData("%WIFI_SCAN_CONTENT%", kScanContent);
registry.registerProgmemData("%WIFI_SSID_PLACEHOLDER%", kSsidPlaceholder);
registry.registerProgmemData("%WIFI_PASSWORD_PLACEHOLDER%", kPasswordPlaceholder);
registry.registerProgmemData("%WIFI_STATIC_FIELDS%", kStaticFields);
registry.registerProgmemData("%WIFI_PARAM_SECTION%", kParamSection);
registry.registerProgmemData("%WIFI_FORM_ACTIONS%", kFormActions);
registry.registerProgmemData("%WIFI_PAGE_ACTIONS%", kPageActions);
registry.registerProgmemData("%WIFI_STATUS%", kStatus);
String output = renderTemplate(WM_WIFI_CONTENT_TEMPLATE, registry);
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='scan-results'"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Scanning for networks..."));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("placeholder='Office'"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='refresh-btn'"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Token"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Save"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back"));
Serial.println("[TEST] WiFi page template rendering test completed successfully");
}
void test_message_template_renders_body_and_actions() {
Serial.println("[TEST] Testing message template rendering...");
PlaceholderRegistry registry(8);
registry.registerProgmemData("%MESSAGE_BODY%", kMessageBody);
registry.registerProgmemData("%MESSAGE_ACTIONS%", kMessageActions);
String output = renderTemplate(WM_MESSAGE_CONTENT_TEMPLATE, registry);
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Saved"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back"));
Serial.println("[TEST] Message template rendering test completed successfully");
}
void test_param_template_renders_shared_actions() {
Serial.println("[TEST] Testing param page template rendering...");
PlaceholderRegistry registry(8);
registry.registerProgmemData("%PARAM_FIELDS%", kParamSection);
registry.registerProgmemData("%PARAM_FORM_ACTIONS%", kFormActions);
registry.registerProgmemData("%PARAM_PAGE_ACTIONS%", kMessageActions);
registry.registerProgmemData("%PARAM_STATUS%", kStatus);
String output = renderTemplate(WM_PARAM_CONTENT_TEMPLATE, registry);
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Token"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Save"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status"));
Serial.println("[TEST] Param page template rendering test completed successfully");
}
void test_info_template_renders_sections_and_footer() {
Serial.println("[TEST] Testing info page template rendering...");
PlaceholderRegistry registry(8);
registry.registerProgmemData("%INFO_STATUS%", kStatus);
registry.registerProgmemData("%INFO_DEVICE_SECTION%", kInfoSection);
registry.registerProgmemData("%INFO_WIFI_SECTION%", kInfoSection);
registry.registerProgmemData("%INFO_ABOUT_SECTION%", kInfoSection);
registry.registerProgmemData("%INFO_FOOTER%", kInfoFooter);
String output = renderTemplate(WM_INFO_CONTENT_TEMPLATE, registry);
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Build date"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Update"));
Serial.println("[TEST] Info page template rendering test completed successfully");
}
void test_fragment_action_form_renders_shared_button_markup() {
Serial.println("[TEST] Testing shared action fragment rendering...");
PlaceholderRegistry registry(8);
registry.registerProgmemData("%ACTION_PREFIX%", kActionPrefix);
registry.registerProgmemData("%ACTION%", kActionPath);
registry.registerProgmemData("%METHOD%", kActionMethod);
registry.registerProgmemData("%BUTTON_LABEL%", kActionLabel);
registry.registerProgmemData("%BUTTON_CLASS_ATTR%", kActionClassAttr);
registry.registerProgmemData("%ACTION_SUFFIX%", kActionSuffix);
String output = renderTemplate(WM_ACTION_FORM_TEMPLATE, registry);
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("action='/update'"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("class='D'"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(">Update</button>"));
Serial.println("[TEST] Shared action fragment rendering test completed successfully");
}
void test_fragment_info_row_and_section_render() {
Serial.println("[TEST] Testing shared info fragment rendering...");
PlaceholderRegistry rowRegistry(4);
rowRegistry.registerProgmemData("%INFO_LABEL%", kInfoLabel);
rowRegistry.registerProgmemData("%INFO_VALUE%", kInfoValue);
String rowOutput = renderTemplate(WM_INFO_ROW_TEMPLATE, rowRegistry);
TEST_ASSERT_NOT_EQUAL(-1, rowOutput.indexOf("<dt>SDK version</dt><dd>3.1.2</dd>"));
PlaceholderRegistry sectionRegistry(6);
sectionRegistry.registerProgmemData("%SECTION_PREFIX%", kEmpty);
sectionRegistry.registerProgmemData("%SECTION_TITLE%", kSectionTitle);
sectionRegistry.registerProgmemData("%SECTION_ROWS%", kSectionRows);
sectionRegistry.registerProgmemData("%SECTION_SUFFIX%", kActionSuffix);
String sectionOutput = renderTemplate(WM_INFO_SECTION_TEMPLATE, sectionRegistry);
TEST_ASSERT_NOT_EQUAL(-1, sectionOutput.indexOf("<h3>Device</h3>"));
TEST_ASSERT_NOT_EQUAL(-1, sectionOutput.indexOf("<dt>Chip ID</dt><dd>abc</dd>"));
Serial.println("[TEST] Shared info fragment rendering test completed successfully");
}
void test_fragment_centered_button_supports_extra_attrs() {
Serial.println("[TEST] Testing centered button fragment extra attrs...");
PlaceholderRegistry registry(8);
registry.registerProgmemData("%BUTTON_ID%", kButtonId);
registry.registerProgmemData("%BUTTON_TYPE%", kButtonType);
registry.registerProgmemData("%BUTTON_ONCLICK%", kButtonOnClick);
registry.registerProgmemData("%BUTTON_LABEL%", kActionLabel);
registry.registerProgmemData("%BUTTON_EXTRA_ATTRS%", kButtonExtraAttrs);
String output = renderTemplate(WM_CENTERED_BUTTON_TEMPLATE, registry);
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='refresh-btn'"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("data-skip-initial-scan='true'"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(">Update</button>"));
Serial.println("[TEST] Centered button fragment extra attrs test completed successfully");
}
void test_root_template_renders_core_placeholders() {
Serial.println("[TEST] Testing root template rendering...");
PlaceholderRegistry registry(8);
registry.registerProgmemData("%SCRIPTS%", kEmpty);
registry.registerProgmemData("%STYLES%", kEmpty);
registry.registerProgmemData("%PAGE_TITLE%", kPageTitle);
registry.registerProgmemData("%SUBTITLE%", kSubtitle);
registry.registerProgmemData("%MENU%", kMenu);
registry.registerProgmemData("%STATUS%", kStatus);
String output = renderTemplate(WM_ROOT_TEMPLATE, registry);
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("WiFiManager"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Setup"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Configure WiFi"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status"));
Serial.println("[TEST] Root template rendering test completed successfully");
}
@@ -3,88 +3,150 @@
#include <WiFiManager.h>
#include "../test_main.h"
// Test WiFi scan initiates
void test_wifi_scan_initiates() {
Serial.println("[TEST] Testing WiFi scan initiation...");
Serial.println("[TEST] Testing async scan request queues...");
WiFiManager wm;
// Initiate scan (non-blocking)
unsigned long start = millis();
(void)WiFi.scanNetworks(true); // true = async
unsigned long elapsed = millis() - start;
// Scan initiation should return quickly (non-blocking)
TEST_ASSERT_LESS_THAN(100, elapsed);
// Return value should be number of networks (if scan completed) or -1 (if async)
// For async scan, it returns -1 immediately
TEST_ASSERT_TRUE_MESSAGE(true, "WiFi scan initiated (return value may be -1 for async)");
Serial.println("[TEST] WiFi scan initiation test completed successfully");
wm.requestAsyncScan(true);
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_QUEUED, wm.getScanState());
TEST_ASSERT_TRUE_MESSAGE(wm.getScanSnapshot().schedulePending,
"Explicit refresh should mark scan scheduling as pending");
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_SCHEDULE_USER_REFRESH,
wm.getScanSnapshot().scheduledReason);
TEST_ASSERT_FALSE_MESSAGE(wm.hasValidScanResults(),
"Queueing a scan should invalidate previous cached results");
Serial.println("[TEST] Async scan request queues test completed successfully");
}
// Test async scan behavior
void test_async_scan_behavior() {
Serial.println("[TEST] Testing async scan behavior...");
Serial.println("[TEST] Testing repeated scan requests coalesce...");
WiFiManager wm;
// Initiate async scan
unsigned long start = millis();
(void)WiFi.scanNetworks(true); // true = async
unsigned long elapsed = millis() - start;
// Should return immediately (non-blocking)
TEST_ASSERT_LESS_THAN(50, elapsed);
// For async scan, result is -1 immediately
// Scan is in progress, status can be checked later
TEST_ASSERT_TRUE_MESSAGE(true, "Async scan returns immediately (non-blocking)");
Serial.println("[TEST] Async scan behavior test completed successfully");
wm.requestAsyncScan(true);
wm.requestAsyncScan(true);
wm.requestAsyncScan(true);
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_QUEUED, wm.getScanState());
TEST_ASSERT_TRUE_MESSAGE(wm.getScanSnapshot().schedulePending,
"Repeated refresh clicks should still leave one pending schedule");
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_SCHEDULE_USER_REFRESH,
wm.getScanSnapshot().scheduledReason);
TEST_ASSERT_FALSE_MESSAGE(wm.hasValidScanResults(),
"Repeated refresh requests should keep a single queued scan lifecycle");
Serial.println("[TEST] Repeated scan requests coalesce test completed successfully");
}
// Test scan status checking (non-blocking)
void test_scan_status_checking() {
Serial.println("[TEST] Testing scan status checking...");
Serial.println("[TEST] Testing cached scan snapshot state...");
WiFiManager wm;
// Initiate scan
WiFi.scanNetworks(true); // async
// Check scan status (should be able to check without blocking)
(void)WiFi.scanComplete();
// Status may be -1 (scanning), -2 (not started), or >= 0 (number of networks)
// Just verify we can check status without blocking
TEST_ASSERT_TRUE_MESSAGE(true, "Scan status can be checked without blocking");
Serial.println("[TEST] Scan status checking test completed successfully");
#ifdef UNIT_TEST
std::vector<WiFiManager::WiFiScanNetwork> results = {
{ "Office", -48, 1 },
{ "Guest", -70, 0 }
};
wm.wmTestInjectScanResults(results);
TEST_ASSERT_TRUE_MESSAGE(wm.hasValidScanResults(),
"Injected scan snapshot should be marked valid");
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_COMPLETE, wm.getScanState());
TEST_ASSERT_EQUAL(static_cast<size_t>(2), wm.getScanResults().size());
TEST_ASSERT_EQUAL_STRING("Office", wm.getScanResults()[0].ssid.c_str());
#else
TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Cached scan snapshot state test completed successfully");
}
// Test scan completion wait
void test_scan_completion_wait() {
Serial.println("[TEST] Testing scan completion wait...");
Serial.println("[TEST] Testing scan timeout transition...");
WiFiManager wm;
// Initiate scan
WiFi.scanNetworks(true); // async
// Wait for scan to complete (with timeout)
unsigned long start = millis();
int scanStatus = -1;
while (scanStatus < 0 && (millis() - start < 10000)) { // 10 second timeout
delay(100);
scanStatus = WiFi.scanComplete();
}
// After wait, status should be >= 0 (completed) or still -1 (timeout)
// Just verify we can wait for completion
TEST_ASSERT_TRUE_MESSAGE(true, "Scan completion can be waited for");
Serial.println("[TEST] Scan completion wait test completed successfully");
#ifdef UNIT_TEST
wm.wmTestSetPortalActive(true);
wm.wmTestForceScanState(WiFiManager::WM_SCAN_RUNNING);
wm.wmTestSetScanStartedAt(millis() - 20000);
wm.wmTestSetScanTimeoutMs(1000);
wm.process();
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_TIMEOUT, wm.getScanState());
TEST_ASSERT_FALSE_MESSAGE(wm.hasValidScanResults(),
"Timed out scans should not leave cached results marked valid");
#else
TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Scan timeout transition test completed successfully");
}
void test_scan_cancels_when_connect_pending() {
Serial.println("[TEST] Testing scan cancellation during connect...");
WiFiManager wm;
#ifdef UNIT_TEST
wm.wmTestSetPortalActive(true);
wm.wmTestForceScanState(WiFiManager::WM_SCAN_RUNNING);
wm.wmTestSetScanStartedAt(millis());
wm.wmTestSetConnectPending(true);
wm.process();
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_IDLE, wm.getScanState());
TEST_ASSERT_FALSE_MESSAGE(wm.getScanSnapshot().schedulePending,
"Connect flow should cancel any in-flight async scan");
#else
TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Scan cancellation during connect test completed successfully");
}
void test_scan_cancels_when_lifecycle_blocked() {
Serial.println("[TEST] Testing scan cancellation during lifecycle block...");
WiFiManager wm;
#ifdef UNIT_TEST
wm.wmTestSetPortalActive(true);
wm.requestAsyncScan(true);
wm.wmTestSetScanLifecycleBlocked(true);
wm.process();
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_IDLE, wm.getScanState());
TEST_ASSERT_FALSE_MESSAGE(wm.getScanSnapshot().schedulePending,
"Lifecycle blocking should leave scan engine idle");
#else
TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Scan cancellation during lifecycle block test completed successfully");
}
void test_scan_generation_invalidated_on_reset() {
Serial.println("[TEST] Testing scan generation invalidation on reset...");
WiFiManager wm;
#ifdef UNIT_TEST
wm.wmTestSetScanGenerations(4, 4, 4);
wm.wmTestSetScanCompletionPending(3);
wm.wmTestClearScanResults();
TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_IDLE, wm.getScanState());
TEST_ASSERT_TRUE_MESSAGE(wm.getScanSnapshot().generation > 4,
"Resetting scan state should invalidate older completion generations");
TEST_ASSERT_FALSE_MESSAGE(wm.getScanSnapshot().completionPending,
"Resetting scan state should clear pending completion callbacks");
#else
TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Scan generation invalidation on reset test completed successfully");
}