Portal customization API v2, SPA hardening, and test cleanup

- Group portal state into brand/pages/actions/layout structs; portal* API
- Bootstrap JSON: nested brand/context/pages/actions/layout; param kind
- Handlers: PROGMEM default CSS when no override/append; dynamic CSS path
- PortalAppJS: portal timeout countdown; safe WiFi scan DOM; serialized
  scan-status polling; fetch error handling; wm:view-changed on redirects
- CSS: scan list icon alignment; wm-status for timeout
- Tests: remove low-value/no-op tests; trim fragile JS substring checks
This commit is contained in:
2026-05-04 22:03:43 +10:00
parent 93711d981d
commit 7735d7f0cb
22 changed files with 1039 additions and 326 deletions
+2 -19
View File
@@ -8,7 +8,6 @@ TestCase tests[] = {
// Basic tests
TEST_ENTRY(test_basic_wifimanager_instantiation),
TEST_ENTRY(test_reset_settings),
TEST_ENTRY(test_disconnect),
// JSON API shape (buildApiInfoJson is large — run with fresh heap, before stress tests)
TEST_ENTRY(test_captive_redirect_host_rule),
TEST_ENTRY(test_api_wifi_meta_json_shape),
@@ -17,9 +16,7 @@ TestCase tests[] = {
TEST_ENTRY(test_api_status_json_shape),
// Configuration tests
TEST_ENTRY(test_configuration_setters),
TEST_ENTRY(test_set_and_get_hostname),
TEST_ENTRY(test_wifi_ap_configuration_setters),
TEST_ENTRY(test_get_default_ap_name),
TEST_ENTRY(test_get_wifi_status_string),
TEST_ENTRY(test_get_mode_string),
@@ -37,19 +34,18 @@ TestCase tests[] = {
TEST_ENTRY(test_config_portal_already_active),
TEST_ENTRY(test_get_config_portal_ssid),
TEST_ENTRY(test_bootstrap_json_portal_feature_flags),
TEST_ENTRY(test_bootstrap_json_contract_v2),
TEST_ENTRY(test_bootstrap_json_snapshot_consistency),
TEST_ENTRY(test_root_render_interleaved_context_isolation),
// Non-blocking tests
TEST_ENTRY(test_nonblocking_process),
TEST_ENTRY(test_client_check_setters),
TEST_ENTRY(test_nonblocking_timeout_behavior),
TEST_ENTRY(test_process_required_for_timeout),
// Callback tests
TEST_ENTRY(test_ap_callback),
TEST_ENTRY(test_web_server_callback),
TEST_ENTRY(test_callback_registration),
TEST_ENTRY(test_multiple_callbacks),
// Parameter tests
@@ -64,21 +60,11 @@ TestCase tests[] = {
// Static IP tests
TEST_ENTRY(test_set_ap_static_ip_config),
TEST_ENTRY(test_sta_static_ip_configuration),
TEST_ENTRY(test_ap_static_ip_application),
// Advanced options tests
TEST_ENTRY(test_connection_behavior_options),
TEST_ENTRY(test_ui_display_options),
TEST_ENTRY(test_ui_customization),
TEST_ENTRY(test_debug_soft_ap_config),
TEST_ENTRY(test_debug_platform_info),
// WiFi connection tests
TEST_ENTRY(test_autoconnect_fallback_to_portal),
TEST_ENTRY(test_connectwifi_ssid_not_found),
TEST_ENTRY(test_connectwifi_retry_count),
TEST_ENTRY(test_connectwifi_timeout_setting),
TEST_ENTRY(test_connection_state_transitions),
TEST_ENTRY(test_autoconnect_with_timeout),
@@ -94,10 +80,6 @@ TestCase tests[] = {
TEST_ENTRY(test_dns_server_lifecycle_cycles),
// Error condition tests
TEST_ENTRY(test_invalid_ap_password_too_short),
TEST_ENTRY(test_invalid_ap_password_too_long),
TEST_ENTRY(test_empty_ssid),
TEST_ENTRY(test_very_long_ssid),
TEST_ENTRY(test_connection_failure_handling),
TEST_ENTRY(test_resource_cleanup_after_error),
TEST_ENTRY(test_multiple_rapid_start_stop),
@@ -120,6 +102,7 @@ TestCase tests[] = {
// Template rendering tests
TEST_ENTRY(test_shell_template_renders_core_placeholders),
TEST_ENTRY(test_shell_template_renders_dynamic_styles_with_percent_values),
// State transition tests
TEST_ENTRY(test_portal_to_connected_transition),
+2 -17
View File
@@ -18,12 +18,9 @@ struct TestCase {
// Basic tests
void test_basic_wifimanager_instantiation();
void test_reset_settings();
void test_disconnect();
// Configuration tests
void test_configuration_setters();
void test_set_and_get_hostname();
void test_wifi_ap_configuration_setters();
void test_get_default_ap_name();
void test_get_wifi_status_string();
void test_get_mode_string();
@@ -41,19 +38,18 @@ void test_config_portal_multiple_start_stop();
void test_config_portal_already_active();
void test_get_config_portal_ssid();
void test_bootstrap_json_portal_feature_flags();
void test_bootstrap_json_contract_v2();
void test_bootstrap_json_snapshot_consistency();
void test_root_render_interleaved_context_isolation();
// Non-blocking tests
void test_nonblocking_process();
void test_client_check_setters();
void test_nonblocking_timeout_behavior();
void test_process_required_for_timeout();
// Callback tests
void test_ap_callback();
void test_web_server_callback();
void test_callback_registration();
void test_multiple_callbacks();
// Parameter tests
@@ -68,21 +64,13 @@ void test_parameter_placeholder();
// Static IP tests
void test_set_ap_static_ip_config();
void test_sta_static_ip_configuration();
void test_ap_static_ip_application();
// Advanced options tests
void test_connection_behavior_options();
void test_ui_display_options();
void test_ui_customization();
void test_debug_soft_ap_config();
void test_debug_platform_info();
// WiFi connection tests
void test_autoconnect_fallback_to_portal();
void test_connectwifi_ssid_not_found();
void test_connectwifi_retry_count();
void test_connectwifi_timeout_setting();
void test_connection_state_transitions();
void test_autoconnect_with_timeout();
@@ -98,10 +86,6 @@ void test_dns_server_cleanup();
void test_dns_server_lifecycle_cycles();
// Error condition tests
void test_invalid_ap_password_too_short();
void test_invalid_ap_password_too_long();
void test_empty_ssid();
void test_very_long_ssid();
void test_connection_failure_handling();
void test_resource_cleanup_after_error();
void test_multiple_rapid_start_stop();
@@ -124,6 +108,7 @@ void test_scan_generation_invalidated_on_reset();
// Template rendering tests
void test_shell_template_renders_core_placeholders();
void test_shell_template_renders_dynamic_styles_with_percent_values();
// API JSON + captive helper tests
void test_captive_redirect_host_rule();
@@ -27,6 +27,18 @@ void test_connection_behavior_options() {
wm.setEnableConfigPortal(false);
wm.setDisableConfigPortal(true);
wm.setDisableConfigPortal(false);
wm.portalSetBehaviorConnectOnSave(true);
wm.portalSetBehaviorConnectOnSave(false);
wm.portalSetBehaviorExitAllowed(true);
wm.portalSetBehaviorExitAllowed(false);
wm.portalSetBehaviorConnectTimeoutSeconds(15);
wm.portalSetBehaviorPortalTimeoutSeconds(20);
wm.portalSetBehaviorAutoReconnect(true);
wm.portalSetBehaviorAutoReconnect(false);
wm.portalSetBehaviorApClientCheck(true);
wm.portalSetBehaviorApClientCheck(false);
wm.portalSetBehaviorWebClientCheck(true);
wm.portalSetBehaviorWebClientCheck(false);
wm.preloadWiFi("TestSSID", "TestPassword");
// All setters executed without crash
@@ -42,16 +54,27 @@ void test_ui_display_options() {
WiFiManager wm;
// Test display toggles
wm.setShowPassword(true);
wm.setShowPassword(false);
wm.portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode::Actual);
wm.portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode::Masked);
wm.portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode::Hidden);
wm.setScanDispPerc(true);
wm.setScanDispPerc(false);
wm.setShowInfoErase(true);
wm.setShowInfoErase(false);
wm.setShowInfoUpdate(true);
wm.setShowInfoUpdate(false);
wm.setParametersEmbeddedInWifi(true);
wm.setParametersEmbeddedInWifi(false);
wm.portalSetActionEraseVisible(true);
wm.portalSetActionEraseVisible(false);
wm.portalSetActionRestartVisible(true);
wm.portalSetActionRestartVisible(false);
wm.portalSetActionExitVisible(true);
wm.portalSetActionExitVisible(false);
wm.portalSetActionCloseCaptiveVisible(true);
wm.portalSetActionCloseCaptiveVisible(false);
wm.portalSetPageUpdateVisible(true);
wm.portalSetPageUpdateVisible(false);
wm.portalSetPageSetupVisible(true);
wm.portalSetPageSetupVisible(false);
wm.portalSetLayoutParamsLocation(PortalParamsLocation::WiFiPage);
wm.portalSetLayoutParamsLocation(PortalParamsLocation::SetupPage);
wm.portalSetActionBackVisible(true);
wm.portalSetActionBackVisible(false);
// All setters executed without crash
TEST_ASSERT_TRUE_MESSAGE(true, "UI display options executed without crash");
@@ -66,7 +89,7 @@ void test_ui_customization() {
WiFiManager wm;
// Minimal customization retained: title
wm.setTitle("MyDevice");
wm.portalSetBrandTitle("MyDevice");
// All setters executed without crash
TEST_ASSERT_TRUE_MESSAGE(true, "UI customization options executed without crash");
@@ -29,6 +29,14 @@ void test_api_wifi_meta_json_shape() {
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"canRefreshScan\":true")));
WiFiManagerParameter fieldParam("mqtt_host", "MQTT host", "broker.local", 64, "placeholder='broker.local'");
WiFiManagerParameter htmlParam("<div class='wm-callout wm-callout--info'><p>GPS settings...</p></div>");
wm.portalAddParameter(&fieldParam);
wm.portalAddParameter(&htmlParam);
j = handlers.buildApiWifiMetaJson();
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"kind\":\"field\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"kind\":\"html\"")));
Serial.println("[TEST] WiFi meta JSON shape test completed successfully");
}
@@ -37,6 +45,12 @@ void test_api_info_json_shape() {
WiFiManager wm;
WiFiManagerHandlers handlers(&wm);
PortalInfoSection battery;
battery.id = "battery";
battery.title = "Battery";
battery.items.push_back({"soc", "State of charge", "84%"});
battery.items.push_back({"voltage", "Voltage", "13.2V"});
wm.portalAddInfoSection(battery);
String j = handlers.buildApiInfoJson();
const char* p = j.c_str();
// Shape checks only: "status" is a nested object (},\n"device" not ],"device"); require
@@ -48,6 +62,10 @@ void test_api_info_json_shape() {
TEST_ASSERT_NOT_NULL(strstr(p, "\"status\":"));
TEST_ASSERT_NOT_NULL(strstr(p, "\"connected\":"));
TEST_ASSERT_NOT_NULL(strstr(p, "},\"device\":["));
TEST_ASSERT_NOT_NULL(strstr(p, "\"extraSections\":"));
TEST_ASSERT_NOT_NULL(strstr(p, "\"id\":\"battery\""));
TEST_ASSERT_NOT_NULL(strstr(p, "\"title\":\"Battery\""));
TEST_ASSERT_NOT_NULL(strstr(p, "\"label\":\"State of charge\""));
TEST_ASSERT_NOT_NULL(strstr(p, "\"showUpdate\":"));
Serial.println("[TEST] API info JSON shape test completed successfully");
+1 -7
View File
@@ -9,10 +9,7 @@ void test_basic_wifimanager_instantiation() {
// Test default constructor
WiFiManager wm;
// Verify instance created (basic sanity check)
TEST_ASSERT_TRUE_MESSAGE(true, "WiFiManager instance created successfully");
// Test that we can call basic methods without crashing
String defaultName = wm.getDefaultAPName();
TEST_ASSERT_GREATER_THAN(0, defaultName.length());
@@ -69,9 +66,6 @@ void test_disconnect() {
// Can be called multiple times safely
(void)wm.disconnect();
// Verify no crash (results may be true/false depending on WiFi state)
TEST_ASSERT_TRUE_MESSAGE(true, "disconnect() executed without crash");
Serial.println("[TEST] disconnect() test completed successfully");
}
@@ -36,26 +36,18 @@ void test_configuration_setters() {
wm.setShowDnsFields(false);
// Test portal behavior
wm.setCaptivePortalEnable(true);
wm.setCaptivePortalEnable(false);
wm.portalSetBehaviorCaptivePortalEnabled(true);
wm.portalSetBehaviorCaptivePortalEnabled(false);
// Test custom title (no getter available, but we can verify it doesn't crash)
wm.setTitle("TestTitle");
// Verify some settings can be retrieved (if getters exist)
// Note: Not all setters have getters, but we verify they don't crash
// All setters executed without crash
TEST_ASSERT_TRUE_MESSAGE(true, "All configuration setters executed without crash");
// Verify setters can be called multiple times with different values
// Test custom title path
wm.portalSetBrandTitle("TestTitle");
// Call setters multiple times with different values.
wm.setConfigPortalTimeout(60);
wm.setConnectTimeout(30);
wm.setHttpPort(8080);
wm.setMinimumSignalQuality(50);
// Verify no crash after multiple calls
TEST_ASSERT_TRUE_MESSAGE(true, "Configuration setters can be called multiple times");
Serial.println("[TEST] Configuration setters test completed successfully");
}
@@ -76,7 +68,6 @@ void test_set_and_get_hostname() {
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");
}
@@ -108,9 +99,6 @@ void test_wifi_ap_configuration_setters() {
wm.setWiFiAutoReconnect(true);
wm.setWiFiAutoReconnect(false);
// All setters executed without crash
TEST_ASSERT_TRUE_MESSAGE(true, "All WiFi AP configuration setters executed without crash");
Serial.println("[TEST] WiFi AP configuration setters test completed successfully");
}
@@ -172,10 +172,7 @@ void test_multiple_rapid_start_stop() {
wm.process();
delay(10);
}
// Should not crash
TEST_ASSERT_TRUE_MESSAGE(true, "Multiple rapid start/stop calls handled without crash");
// Final state should be stopped
TEST_ASSERT_FALSE(wm.getConfigPortalActive());
@@ -70,7 +70,7 @@ void test_parameter_add_and_retrieve() {
// 1. Add parameters
WiFiManagerParameter param("server", "Server", "default", 40);
wm.addParameter(&param);
wm.portalAddParameter(&param);
TEST_ASSERT_EQUAL(1, wm.getParametersCount());
@@ -135,8 +135,8 @@ void test_portal_with_parameters_and_infrastructure() {
// Add parameters
WiFiManagerParameter p1("server", "Server", "192.168.1.1", 40);
WiFiManagerParameter p2("port", "Port", "1883", 6);
wm.addParameter(&p1);
wm.addParameter(&p2);
wm.portalAddParameter(&p1);
wm.portalAddParameter(&p2);
// Start portal
wm.startConfigPortal("TestAP");
@@ -44,7 +44,7 @@ void test_parameter_value_persistence() {
WiFiManager wm;
WiFiManagerParameter param("server", "Server", "default", 40);
wm.addParameter(&param);
wm.portalAddParameter(&param);
// Set value
param.setValue("persisted_value", 40);
@@ -87,9 +87,9 @@ void test_multiple_parameters_different_values() {
WiFiManagerParameter p2("port", "Port", "1883", 6);
WiFiManagerParameter p3("key", "Key", "default_key", 32);
wm.addParameter(&p1);
wm.addParameter(&p2);
wm.addParameter(&p3);
wm.portalAddParameter(&p1);
wm.portalAddParameter(&p2);
wm.portalAddParameter(&p3);
// Set different values
p1.setValue("10.0.0.1", 40);
+12 -10
View File
@@ -20,7 +20,7 @@ void test_create_parameter() {
}
void test_add_parameter() {
Serial.println("[TEST] Testing addParameter()...");
Serial.println("[TEST] Testing portalAddParameter()...");
WiFiManager wm;
@@ -28,16 +28,18 @@ void test_add_parameter() {
WiFiManagerParameter param2("param2", "Parameter 2", "value2", 40);
// Add parameters
bool result1 = wm.addParameter(&param1);
bool result2 = wm.addParameter(&param2);
bool result1 = wm.portalAddParameter(&param1);
bool result2 = wm.portalAddParameter(&param2);
TEST_ASSERT_TRUE(result1);
TEST_ASSERT_TRUE(result2);
// Verify parameters are stored
TEST_ASSERT_EQUAL(2, wm.getParametersCount());
wm.portalClearParameters();
TEST_ASSERT_EQUAL(0, wm.getParametersCount());
Serial.println("[TEST] addParameter() test completed successfully");
Serial.println("[TEST] portalAddParameter() test completed successfully");
}
void test_get_parameters() {
@@ -49,9 +51,9 @@ void test_get_parameters() {
WiFiManagerParameter param2("param2", "Parameter 2", "value2", 40);
WiFiManagerParameter param3("param3", "Parameter 3", "value3", 40);
wm.addParameter(&param1);
wm.addParameter(&param2);
wm.addParameter(&param3);
wm.portalAddParameter(&param1);
wm.portalAddParameter(&param2);
wm.portalAddParameter(&param3);
// Get count
int count = wm.getParametersCount();
@@ -121,9 +123,9 @@ void test_multiple_parameters() {
WiFiManagerParameter p2("mqtt_port", "MQTT Port", "1883", 6);
WiFiManagerParameter p3("api_key", "API Key", "default_key", 32);
wm.addParameter(&p1);
wm.addParameter(&p2);
wm.addParameter(&p3);
wm.portalAddParameter(&p1);
wm.portalAddParameter(&p2);
wm.portalAddParameter(&p3);
TEST_ASSERT_EQUAL(3, wm.getParametersCount());
@@ -19,7 +19,7 @@ size_t dynamicStringLengthGetter(const char* data, void* /*userData*/) {
return data ? strlen(data) : 0;
}
void configureDescriptor(DynamicTemplateDescriptor& descriptor, String& value) {
void configureDescriptor(DynamicDataDescriptor& descriptor, String& value) {
descriptor.getter = &dynamicStringGetter;
descriptor.getLength = &dynamicStringLengthGetter;
descriptor.userData = &value;
@@ -62,20 +62,77 @@ void test_bootstrap_json_portal_feature_flags() {
WiFiManagerHandlers handlers(&wm);
String idleBootstrap = handlers.buildPortalBootstrapJson();
TEST_ASSERT_EQUAL(-1, idleBootstrap.indexOf(F("\"showCloseCaptive\":true")));
TEST_ASSERT_EQUAL(-1, idleBootstrap.indexOf(F("\"closeCaptive\":{\"visible\":true")));
wm.wmTestSetPortalActive(true);
wm.setCaptivePortalEnable(true);
wm.portalSetBehaviorCaptivePortalEnabled(true);
String apBootstrap = handlers.buildPortalBootstrapJson();
TEST_ASSERT_NOT_EQUAL(-1, apBootstrap.indexOf(F("\"showCloseCaptive\":true")));
TEST_ASSERT_NOT_EQUAL(-1, apBootstrap.indexOf(F("\"showExitPortal\":true")));
TEST_ASSERT_NOT_EQUAL(-1, apBootstrap.indexOf(F("\"closeCaptive\":{\"visible\":true")));
TEST_ASSERT_NOT_EQUAL(-1, apBootstrap.indexOf(F("\"exitPortal\":{\"visible\":true")));
wm.wmTestSetPortalActive(false);
Serial.println("[TEST] Bootstrap portal feature flags test completed successfully");
}
void test_bootstrap_json_contract_v2() {
Serial.println("[TEST] Testing bootstrap JSON v2 contract...");
WiFiManager wm;
WiFiManagerHandlers handlers(&wm);
wm.portalSetBrandTitle("Solar Battery Monitor Setup");
wm.portalSetContextIdentityText("Solar Battery Monitor");
wm.portalSetBrandHomeIntro("Connect this device to WiFi and finish setup.");
wm.portalSetBrandLogoSvg("<svg viewBox='0 0 24 24'></svg>");
wm.portalSetPageInfoVisible(true);
wm.portalSetPageUpdateVisible(false);
wm.portalSetPageSetupVisible(true);
wm.portalSetActionEraseVisible(false);
wm.portalSetActionBackVisible(true);
wm.portalSetLayoutParamsLocation(PortalParamsLocation::SetupPage);
wm.setConfigPortalTimeout(90);
PortalHomeCard card;
card.id = "solar";
card.title = "Solar summary";
card.kind = PortalHomeCardKind::KeyValue;
card.items.push_back({"pv", "PV input", "420W"});
wm.portalAddHomeCard(card);
String j = handlers.buildPortalBootstrapJson();
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"contractVersion\":2")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"brand\":{")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"title\":\"Solar Battery Monitor Setup\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"homeIntro\":\"Connect this device to WiFi and finish setup.\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"logoSvg\":\"<svg viewBox='0 0 24 24'></svg>\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"context\":{")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"identityText\":\"Solar Battery Monitor\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"pages\":{")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"setup\":{\"visible\":true}")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\":{")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"erase\":{\"visible\":false}")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"back\":{\"visible\":true}")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"layout\":{\"paramsLocation\":\"setup\"}")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"extraHomeCards\":[")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"id\":\"solar\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"kind\":\"kv\"")));
wm.startWebPortal();
j = handlers.buildPortalBootstrapJson();
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"portalActive\":true")));
wm.stopWebPortal();
wm.wmTestSetPortalActive(true);
j = handlers.buildPortalBootstrapJson();
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"portalTimeoutSecondsRemaining\":")));
TEST_ASSERT_EQUAL(-1, j.indexOf(F("\"portalTimeoutSecondsRemaining\":0")));
wm.wmTestSetPortalActive(false);
Serial.println("[TEST] Bootstrap JSON v2 contract test completed successfully");
}
void test_bootstrap_json_snapshot_consistency() {
Serial.println("[TEST] Testing bootstrap JSON snapshot consistency...");
@@ -106,32 +163,34 @@ void test_root_render_interleaved_context_isolation() {
registryA.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
registryA.registerProgmemData("%PAGE_TITLE%", kTestTitle);
registryA.registerProgmemData("%PORTAL_APPEND_JS%", kEmptyTemplateChunk);
registryB.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
registryB.registerProgmemData("%PAGE_TITLE%", kTestTitle);
registryB.registerProgmemData("%PORTAL_APPEND_JS%", kEmptyTemplateChunk);
String bootstrapA = F("{\"ctx\":\"A\"}");
String appJsA = F("// shell A");
String bootstrapB = F("{\"ctx\":\"B\"}");
String appJsB = F("// shell B");
DynamicTemplateDescriptor bootstrapDescriptorA{};
DynamicTemplateDescriptor appJsDescriptorA{};
DynamicTemplateDescriptor bootstrapDescriptorB{};
DynamicTemplateDescriptor appJsDescriptorB{};
DynamicDataDescriptor bootstrapDescriptorA{};
DynamicDataDescriptor appJsDescriptorA{};
DynamicDataDescriptor bootstrapDescriptorB{};
DynamicDataDescriptor appJsDescriptorB{};
configureDescriptor(bootstrapDescriptorA, bootstrapA);
configureDescriptor(appJsDescriptorA, appJsA);
configureDescriptor(bootstrapDescriptorB, bootstrapB);
configureDescriptor(appJsDescriptorB, appJsB);
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bootstrapDescriptorA),
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicData("%BOOTSTRAP_JSON%", &bootstrapDescriptorA),
"Registry A bootstrap placeholder should register");
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicTemplate("%PORTAL_APP_JS%", &appJsDescriptorA),
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicData("%PORTAL_APP_JS%", &appJsDescriptorA),
"Registry A app JS placeholder should register");
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bootstrapDescriptorB),
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicData("%BOOTSTRAP_JSON%", &bootstrapDescriptorB),
"Registry B bootstrap placeholder should register");
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicTemplate("%PORTAL_APP_JS%", &appJsDescriptorB),
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicData("%PORTAL_APP_JS%", &appJsDescriptorB),
"Registry B app JS placeholder should register");
TemplateContext contextA;
+1 -1
View File
@@ -112,7 +112,7 @@ void test_multiple_parameters_stress() {
sprintf(id, "param%d", i);
sprintf(label, "Parameter %d", i);
params[i] = new WiFiManagerParameter(id, label, "default", 40);
wm.addParameter(params[i]);
wm.portalAddParameter(params[i]);
}
TEST_ASSERT_EQUAL(20, wm.getParametersCount());
@@ -2,6 +2,7 @@
#include <Arduino.h>
#include <TemplateEngine.h>
#include "templates/RootShell.h"
#include "templates/PortalAppJS.h"
#include "../test_main.h"
namespace {
@@ -27,6 +28,22 @@ const char kEmpty[] PROGMEM = "";
const char kDocTitle[] PROGMEM = "Config ESP";
const char kBootstrapJson[] PROGMEM = "{\"title\":\"Test\"}";
const char kPortalAppJs[] PROGMEM = "console.log('portal');";
const char kPortalAppendJs[] PROGMEM = "";
const char* dynamicStringGetter(void* userData) {
const auto* value = static_cast<const String*>(userData);
return value ? value->c_str() : "";
}
size_t dynamicStringLengthGetter(const char* data, void* /*userData*/) {
return data ? strlen(data) : 0;
}
void configureDescriptor(DynamicDataDescriptor& descriptor, String& value) {
descriptor.getter = &dynamicStringGetter;
descriptor.getLength = &dynamicStringLengthGetter;
descriptor.userData = &value;
}
} // namespace
@@ -38,6 +55,7 @@ void test_shell_template_renders_core_placeholders() {
registry.registerProgmemData("%PAGE_TITLE%", kDocTitle);
registry.registerProgmemData("%BOOTSTRAP_JSON%", kBootstrapJson);
registry.registerProgmemData("%PORTAL_APP_JS%", kPortalAppJs);
registry.registerProgmemData("%PORTAL_APPEND_JS%", kPortalAppendJs);
String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry);
@@ -51,3 +69,36 @@ void test_shell_template_renders_core_placeholders() {
Serial.println("[TEST] Portal shell template rendering test completed successfully");
}
void test_shell_template_renders_dynamic_styles_with_percent_values() {
Serial.println("[TEST] Testing portal shell dynamic styles placeholder with percent values...");
PlaceholderRegistry registry(8);
String styles = F("<style>body{width:100%;max-width:92%}</style>");
String title = F("Config ESP");
String bootstrap = F("{\"title\":\"Test\"}");
String appendJs = F("");
DynamicDataDescriptor stylesDescriptor{};
DynamicDataDescriptor titleDescriptor{};
DynamicDataDescriptor bootstrapDescriptor{};
DynamicDataDescriptor appendJsDescriptor{};
configureDescriptor(stylesDescriptor, styles);
configureDescriptor(titleDescriptor, title);
configureDescriptor(bootstrapDescriptor, bootstrap);
configureDescriptor(appendJsDescriptor, appendJs);
TEST_ASSERT_TRUE(registry.registerDynamicData("%STYLES%", &stylesDescriptor));
TEST_ASSERT_TRUE(registry.registerDynamicData("%PAGE_TITLE%", &titleDescriptor));
TEST_ASSERT_TRUE(registry.registerDynamicData("%BOOTSTRAP_JSON%", &bootstrapDescriptor));
TEST_ASSERT_TRUE(registry.registerProgmemData("%PORTAL_APP_JS%", kPortalAppJs));
TEST_ASSERT_TRUE(registry.registerDynamicData("%PORTAL_APPEND_JS%", &appendJsDescriptor));
String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry);
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<style>body{width:100%;max-width:92%}</style>"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<title>Config ESP</title>"));
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("{\"title\":\"Test\"}"));
Serial.println("[TEST] Dynamic styles placeholder percent rendering test completed successfully");
}
@@ -57,7 +57,7 @@ void test_scan_status_checking() {
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");
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Cached scan snapshot state test completed successfully");
@@ -79,7 +79,7 @@ void test_scan_completion_wait() {
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");
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Scan timeout transition test completed successfully");
@@ -101,7 +101,7 @@ void test_scan_cancels_when_connect_pending() {
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");
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Scan cancellation during connect test completed successfully");
@@ -122,7 +122,7 @@ void test_scan_cancels_when_lifecycle_blocked() {
TEST_ASSERT_FALSE_MESSAGE(wm.getScanSnapshot().schedulePending,
"Lifecycle blocking should leave scan engine idle");
#else
TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable");
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Scan cancellation during lifecycle block test completed successfully");
@@ -144,7 +144,7 @@ void test_scan_generation_invalidated_on_reset() {
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");
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Scan generation invalidation on reset test completed successfully");