Release v3.1.0

This commit is contained in:
2026-09-01 10:59:25 +10:00
parent 25977a7320
commit c529fc15a8
30 changed files with 628 additions and 248 deletions
+3 -1
View File
@@ -20,11 +20,13 @@ lib_deps =
[env:esp32]
extends = common
platform = espressif32@6.13.0
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
board = esp32dev
build_unflags = -std=gnu++11
build_flags =
-std=gnu++14
-DSOC_WIFI_SUPPORTED=1
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
-DWM_LOG_LEVEL=3
lib_deps =
${common.lib_deps}
+36
View File
@@ -1,9 +1,45 @@
#include <Arduino.h>
#include <WiFiManager.h>
namespace {
const char kTitle[] PROGMEM = "Set up Compile Fixture";
const char kIdentity[] PROGMEM = "WiFiManager";
const char kIntro[] PROGMEM = "A branded portal compile check.";
const char kLogoAlt[] PROGMEM = "WiFiManager";
const char kLogo[] PROGMEM = "<svg viewBox='0 0 24 24'></svg>";
const char kPage[] PROGMEM = "#f4f7f3";
const char kSurface[] PROGMEM = "#ffffff";
const char kText[] PROGMEM = "#1c251e";
const char kMuted[] PROGMEM = "#607064";
const char kBorder[] PROGMEM = "#d6e0d7";
const char kAccent[] PROGMEM = "#347a45";
const char kAccentHover[] PROGMEM = "#245a32";
const char kAccentText[] PROGMEM = "#ffffff";
const WiFiManagerPortalConfig kPortalConfig = {
WiFiManagerPortalText::progmem(kTitle),
WiFiManagerPortalText::progmem(kIdentity),
WiFiManagerPortalText::progmem(kIntro),
WiFiManagerPortalAsset::svgFromProgmem(kLogo),
WiFiManagerPortalText::progmem(kLogoAlt),
{
WiFiManagerPortalText::progmem(kPage),
WiFiManagerPortalText::progmem(kSurface),
WiFiManagerPortalText::progmem(kText),
WiFiManagerPortalText::progmem(kMuted),
WiFiManagerPortalText::progmem(kBorder),
WiFiManagerPortalText::progmem(kAccent),
WiFiManagerPortalText::progmem(kAccentHover),
WiFiManagerPortalText::progmem(kAccentText),
{}, {}, {}, 10, 6,
},
};
} // namespace
WiFiManager wifiManager;
void setup() {
wifiManager.setPortalConfig(kPortalConfig);
wifiManager.setConfigPortalTimeout(1);
}
+3 -1
View File
@@ -36,9 +36,11 @@ 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_portal_default_presentation),
TEST_ENTRY(test_bootstrap_json_contract_v2),
TEST_ENTRY(test_bootstrap_json_snapshot_consistency),
TEST_ENTRY(test_root_render_interleaved_context_isolation),
TEST_ENTRY(test_portal_presentation_configuration),
// Non-blocking tests
TEST_ENTRY(test_nonblocking_process),
@@ -104,7 +106,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),
TEST_ENTRY(test_shell_template_renders_dynamic_theme_with_percent_values),
// State transition tests
TEST_ENTRY(test_portal_to_connected_transition),
+3 -1
View File
@@ -38,9 +38,11 @@ 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_portal_default_presentation();
void test_bootstrap_json_contract_v2();
void test_bootstrap_json_snapshot_consistency();
void test_root_render_interleaved_context_isolation();
void test_portal_presentation_configuration();
// Non-blocking tests
void test_nonblocking_process();
@@ -108,7 +110,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();
void test_shell_template_renders_dynamic_theme_with_percent_values();
// API JSON + captive helper tests
void test_captive_redirect_host_rule();
@@ -88,8 +88,10 @@ void test_ui_customization() {
WiFiManager wm;
// Minimal customization retained: title
wm.portalSetBrandTitle("MyDevice");
// Minimal customization uses the complete static portal configuration.
WiFiManagerPortalConfig portal;
portal.title = WiFiManagerPortalText::ram("MyDevice");
TEST_ASSERT_TRUE(wm.setPortalConfig(portal));
// All setters executed without crash
TEST_ASSERT_TRUE_MESSAGE(true, "UI customization options executed without crash");
@@ -39,8 +39,10 @@ void test_configuration_setters() {
wm.portalSetBehaviorCaptivePortalEnabled(true);
wm.portalSetBehaviorCaptivePortalEnabled(false);
// Test custom title path
wm.portalSetBrandTitle("TestTitle");
// Test the typed portal presentation path.
WiFiManagerPortalConfig portal;
portal.title = WiFiManagerPortalText::ram("TestTitle");
TEST_ASSERT_TRUE(wm.setPortalConfig(portal));
// Call setters multiple times with different values.
wm.setConfigPortalTimeout(60);
@@ -76,16 +76,29 @@ void test_bootstrap_json_portal_feature_flags() {
Serial.println("[TEST] Bootstrap portal feature flags test completed successfully");
}
void test_portal_default_presentation() {
WiFiManager wm;
WiFiManagerHandlers handlers(&wm);
const String bootstrap = handlers.buildPortalBootstrapJson();
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"title\":\"WiFiManager\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"homeIntro\":\"\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"logoSvg\":\"\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"logoAltText\":\"\"")));
}
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>");
WiFiManagerPortalConfig portal;
portal.title = WiFiManagerPortalText::ram("Solar Battery Monitor Setup");
portal.identityText = WiFiManagerPortalText::ram("Solar Battery Monitor");
portal.homeIntro = WiFiManagerPortalText::ram("Connect this device to WiFi and finish setup.");
portal.logo = WiFiManagerPortalAsset::svgFromRam("<svg viewBox='0 0 24 24'></svg>");
TEST_ASSERT_TRUE(wm.setPortalConfig(portal));
wm.portalSetPageInfoVisible(true);
wm.portalSetPageUpdateVisible(false);
wm.portalSetPageSetupVisible(true);
@@ -163,11 +176,11 @@ void test_root_render_interleaved_context_isolation() {
registryA.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
registryA.registerProgmemData("%PAGE_TITLE%", kTestTitle);
registryA.registerProgmemData("%PORTAL_APPEND_JS%", kEmptyTemplateChunk);
registryA.registerProgmemData("%PORTAL_THEME%", kEmptyTemplateChunk);
registryB.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
registryB.registerProgmemData("%PAGE_TITLE%", kTestTitle);
registryB.registerProgmemData("%PORTAL_APPEND_JS%", kEmptyTemplateChunk);
registryB.registerProgmemData("%PORTAL_THEME%", kEmptyTemplateChunk);
String bootstrapA = F("{\"ctx\":\"A\"}");
String appJsA = F("// shell A");
@@ -217,3 +230,51 @@ void test_root_render_interleaved_context_isolation() {
Serial.println("[TEST] Root render interleaved context isolation test completed successfully");
}
void test_portal_presentation_configuration() {
Serial.println("[TEST] Testing portal presentation configuration...");
WiFiManager wm;
WiFiManagerHandlers handlers(&wm);
WiFiManagerPortalConfig config;
config.title = WiFiManagerPortalText::ram("Set up Temperature Monitor");
config.identityText = WiFiManagerPortalText::ram("Tree");
config.homeIntro = WiFiManagerPortalText::ram("Connect this device to WiFi.");
config.logo = WiFiManagerPortalAsset::svgFromRam("<svg viewBox='0 0 24 24'></svg>");
config.logoAltText = WiFiManagerPortalText::ram("Tree logo");
config.theme.pageBackground = WiFiManagerPortalText::ram("#f4f7f3");
config.theme.surface = WiFiManagerPortalText::ram("#ffffff");
config.theme.text = WiFiManagerPortalText::ram("#1c251e");
config.theme.mutedText = WiFiManagerPortalText::ram("#607064");
config.theme.border = WiFiManagerPortalText::ram("#d6e0d7");
config.theme.accent = WiFiManagerPortalText::ram("#347a45");
config.theme.accentHover = WiFiManagerPortalText::ram("#245a32");
config.theme.accentText = WiFiManagerPortalText::ram("#ffffff");
config.theme.success = WiFiManagerPortalText::ram("#2f855a");
config.theme.danger = WiFiManagerPortalText::ram("#c53030");
config.theme.dangerHover = WiFiManagerPortalText::ram("#9b2c2c");
config.theme.cornerRadiusPx = 10;
config.theme.smallCornerRadiusPx = 6;
TEST_ASSERT_TRUE_MESSAGE(wm.setPortalConfig(config),
"A complete setup-time portal configuration should be accepted");
String bootstrap = handlers.buildPortalBootstrapJson();
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"title\":\"Set up Temperature Monitor\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"identityText\":\"Tree\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"logoAltText\":\"Tree logo\"")));
WiFiManagerPortalConfig unsafeConfig = config;
unsafeConfig.theme.accent = WiFiManagerPortalText::ram("#347a45;body{display:none}");
TEST_ASSERT_FALSE_MESSAGE(wm.setPortalConfig(unsafeConfig),
"Semantic theme values must reject stylesheet injection characters");
wm.startWebPortal();
TEST_ASSERT_FALSE_MESSAGE(wm.setPortalConfig(config),
"Portal presentation must remain immutable while async routes are active");
wm.stopWebPortal();
TEST_ASSERT_TRUE_MESSAGE(wm.setPortalConfig(config),
"Presentation can be applied after the portal stops");
Serial.println("[TEST] Portal presentation configuration test completed successfully");
}
@@ -28,7 +28,7 @@ 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 kPortalTheme[] PROGMEM = "";
const char* dynamicStringGetter(void* userData) {
const auto* value = static_cast<const String*>(userData);
@@ -55,7 +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);
registry.registerProgmemData("%PORTAL_THEME%", kPortalTheme);
String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry);
@@ -70,35 +70,32 @@ 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...");
void test_shell_template_renders_dynamic_theme_with_percent_values() {
Serial.println("[TEST] Testing portal shell dynamic theme placeholder with percent values...");
PlaceholderRegistry registry(8);
String styles = F("<style>body{width:100%;max-width:92%}</style>");
String theme = F("<style>:root{--wm-bg:rgb(20,50,30);--wm-width:100%}</style>");
String title = F("Config ESP");
String bootstrap = F("{\"title\":\"Test\"}");
String appendJs = F("");
DynamicDataDescriptor stylesDescriptor{};
DynamicDataDescriptor themeDescriptor{};
DynamicDataDescriptor titleDescriptor{};
DynamicDataDescriptor bootstrapDescriptor{};
DynamicDataDescriptor appendJsDescriptor{};
configureDescriptor(stylesDescriptor, styles);
configureDescriptor(themeDescriptor, theme);
configureDescriptor(titleDescriptor, title);
configureDescriptor(bootstrapDescriptor, bootstrap);
configureDescriptor(appendJsDescriptor, appendJs);
TEST_ASSERT_TRUE(registry.registerDynamicData("%STYLES%", &stylesDescriptor));
TEST_ASSERT_TRUE(registry.registerProgmemData("%STYLES%", kEmpty));
TEST_ASSERT_TRUE(registry.registerDynamicData("%PORTAL_THEME%", &themeDescriptor));
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("<style>:root{--wm-bg:rgb(20,50,30);--wm-width:100%}</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");
Serial.println("[TEST] Dynamic theme placeholder percent rendering test completed successfully");
}