diff --git a/README.md b/README.md
index 9664714..4b7facd 100644
--- a/README.md
+++ b/README.md
@@ -30,9 +30,10 @@ This fork currently includes the following architectural improvements:
- **Captive portal redirect handling** retained while removing duplicate legacy UI architecture.
- **Data-first JSON generation** for portal APIs, including info/device/about data, instead of HTML-to-JSON parsing.
- **Capability-driven UI flags** in bootstrap/API payloads so features like info, update, erase, and action visibility can be controlled by backend state.
+- **Portal bootstrap contract v2**: nested `brand`, `context`, `pages`, `actions`, `layout`, `extraHomeCards`; Wi-Fi meta params include `kind` (`field` | `html`) for first-class custom HTML parameters.
- **SPA-native feedback UX** using in-DOM dialog/toast behavior rather than page-based action flows.
-- **Request-scoped shell rendering**: the root portal page is built for each `GET /` from `WM_ROOT_SHELL_TEMPLATE` using a fresh placeholder registry. Shell inputs are only `%PAGE_TITLE%`, `%STYLES%`, `%BOOTSTRAP_JSON%`, and `%PORTAL_APP_JS%` — filled in `WiFiManagerHandlers` from WiFiManager state and embedded assets (not from a server-wide template registry).
-- **Customization via WiFiManager APIs** (`setTitle`, `setShowInfo`, `setParametersEmbeddedInWifi`, bootstrap-facing flags, etc.), not by exposing placeholder-registry mutation to consumers.
+- **Request-scoped shell rendering**: the root portal page is built for each `GET /` from `WM_ROOT_SHELL_TEMPLATE` using a fresh placeholder registry. Shell inputs are `%PAGE_TITLE%`, `%STYLES%`, `%BOOTSTRAP_JSON%`, `%PORTAL_APP_JS%`, and `%PORTAL_APPEND_JS%` — filled in `WiFiManagerHandlers` from WiFiManager state and embedded assets (not from a server-wide template registry).
+- **Customization via WiFiManager `portal*` APIs** (`portalSetBrandTitle`, `portalSetPageInfoVisible`, `portalSetLayoutParamsLocation`, `portalAddParameter`, asset hooks, etc.) and JSON under `/api/...`, not by exposing placeholder-registry mutation to consumers.
- A clearer separation between:
- shell rendering (handlers + SPA bootstrap)
- JSON API responses
@@ -40,6 +41,105 @@ This fork currently includes the following architectural improvements:
- OTA handling
- Updated tests focused on the **shell contract**, **bootstrap payloads**, and **API JSON shapes** rather than removed legacy portal pages.
+## Portal Customization Boundary
+
+Stable, supported portal customization is intentionally scoped:
+
+- `portalSetBrand*` for title, intro text, and logo SVG, plus `portalSetContextIdentityText(...)` for the user-facing runtime identity string on the home view.
+- `portalSetPage*`, `portalSetAction*`, `portalSetLayout*`, and `portalSetBehavior*` for built-in portal capabilities and runtime behavior.
+- `portalAddParameter(...)` for first-class custom parameters, including raw HTML blocks inside parameter-rendering surfaces (`#/wifi` or `#/setup`).
+- `portalAddInfoSection(...)` and `portalAddHomeCard(...)` for structured extra content rendered by the built-in SPA.
+- `portalAppendCss(...)`, `portalOverrideCss(...)`, and `portalAppendJs(...)` for light theming and enhancement hooks.
+
+Not part of the stable API:
+
+- arbitrary HTML injection into home/info/nav/shell
+- replacing built-in SPA routing or action flow
+- depending on undocumented DOM IDs or route internals
+- treating `include/templates/*` as a supported consumer override surface
+
+Appended JS should enhance rather than replace the built-in SPA. The documented hook contract is:
+
+- `wm:ready` with `detail.boot`
+- `wm:view-changed` with `detail.route`
+
+If a consumer needs custom live widgets, new primary navigation concepts, or new backend-to-frontend workflows, that is considered **fork territory** rather than portal customization.
+
+## Portal Customization Example
+
+```cpp
+WiFiManager wm;
+
+wm.portalSetBrandTitle("Solar Battery Monitor Setup");
+wm.portalSetContextIdentityText("Solar Battery Monitor");
+wm.portalSetBrandHomeIntro(
+ "Connect your monitor to WiFi, then review battery and inverter settings."
+);
+wm.portalSetBrandLogoSvg(
+ ""
+ " "
+ " "
+);
+
+wm.portalSetPageInfoVisible(true);
+wm.portalSetPageUpdateVisible(false);
+wm.portalSetActionEraseVisible(false);
+wm.portalSetActionRestartVisible(true);
+wm.portalSetLayoutParamsLocation(PortalParamsLocation::SetupPage);
+
+wm.portalSetBehaviorCaptivePortalEnabled(true);
+wm.portalSetBehaviorConnectOnSave(true);
+wm.portalSetBehaviorExitAllowed(true);
+
+wm.portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode::Masked);
+wm.portalSetFieldStaticIpVisibility(PortalFieldVisibility::Auto);
+wm.portalSetFieldStaticDnsVisibility(PortalFieldVisibility::Auto);
+
+WiFiManagerParameter mqttHost(
+ "mqtt_host",
+ "MQTT host",
+ "broker.local",
+ 64,
+ "placeholder='broker.local'"
+);
+wm.portalAddParameter(&mqttHost);
+
+// Raw HTML remains first-class for custom parameters, but only inside the
+// parameter-rendering surfaces (#/wifi or #/setup), not arbitrary portal regions.
+WiFiManagerParameter gpsHelp(
+ "
"
+ "
GPS options below apply only when a GPS module is connected.
"
+ "
"
+);
+wm.portalAddParameter(&gpsHelp);
+
+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);
+
+PortalHomeCard solar;
+solar.id = "solar";
+solar.title = "Solar summary";
+solar.kind = PortalHomeCardKind::KeyValue;
+solar.items.push_back({"pv", "PV input", "420W"});
+solar.items.push_back({"load", "Load", "180W"});
+wm.portalAddHomeCard(solar);
+
+wm.portalAppendCss(
+ ".wm-brand-logo svg{width:40px;height:40px;display:block;}"
+ ".wm-hero-intro{max-width:42ch;}"
+);
+
+wm.portalAppendJs(
+ "document.addEventListener('wm:ready', function(e){"
+ " console.log('Portal booted', e.detail.boot);"
+ "});"
+);
+```
+
## Dependencies
This fork depends on **DFTE** ([Device Framework Template Engine](https://github.com/alexhopeoconnor/DFTE)) and **ESP32Async/ESPAsyncWebServer**.
diff --git a/lib/WiFiManager/include/WiFiManager.h b/lib/WiFiManager/include/WiFiManager.h
index a8edd8f..3e2d8c9 100644
--- a/lib/WiFiManager/include/WiFiManager.h
+++ b/lib/WiFiManager/include/WiFiManager.h
@@ -200,6 +200,74 @@ class WiFiManagerHandlers;
class WiFiManagerDfteLogger;
class WiFiManagerLogSink;
+// ---------------------------------------------------------------------------
+// Portal customization (v2 contract: nested JSON; all entry points use portal* API)
+// ---------------------------------------------------------------------------
+enum class PortalParamsLocation : uint8_t { WiFiPage = 0, SetupPage = 1 };
+
+enum class PortalPasswordPlaceholderMode : uint8_t { Hidden = 0, Masked = 1, Actual = 2 };
+
+enum class PortalFieldVisibility : uint8_t { Hidden = 0, Auto = 1, Always = 2 };
+
+enum class PortalHomeCardKind : uint8_t { Text = 0, Callout = 1, KeyValue = 2 };
+
+struct PortalKeyValueItem {
+ String key;
+ String label;
+ String value;
+};
+
+struct PortalInfoSection {
+ String id;
+ String title;
+ std::vector items;
+};
+
+struct PortalHomeCard {
+ String id;
+ String title;
+ PortalHomeCardKind kind = PortalHomeCardKind::KeyValue;
+ String text;
+ std::vector items;
+};
+
+struct PortalBrandState {
+ String title = "WiFiManager";
+ String identityTextOverride;
+ String homeIntro;
+ String logoSvg;
+};
+
+struct PortalPageState {
+ bool infoVisible = true;
+ bool updateVisible = true;
+ bool setupVisible = true;
+};
+
+struct PortalActionState {
+ bool eraseVisible = true;
+ bool restartVisible = true;
+ bool exitVisible = true;
+ bool closeCaptiveVisible = true;
+ bool backVisible = false;
+};
+
+struct PortalLayoutState {
+ /** When true, custom parameters render on the Wi-Fi page; when false, only on #/setup. */
+ bool paramsOnWifiPage = true;
+};
+
+struct PortalAssetState {
+ String appendedCss;
+ String overriddenCss;
+ String appendedJs;
+};
+
+struct PortalStructuredExtrasState {
+ std::vector infoSections;
+ std::vector homeCards;
+};
+
class WiFiManager
{
public:
@@ -322,7 +390,7 @@ class WiFiManager
bool erase(bool opt);
//adds a custom parameter, returns false on failure
- bool addParameter(WiFiManagerParameter *p);
+ bool portalAddParameter(WiFiManagerParameter *p);
//returns the list of Parameters
WiFiManagerParameter** getParameters();
@@ -421,12 +489,6 @@ class WiFiManager
//if true, always show static dns, esle only show if set via setSTAStaticIPConfig
void setShowDnsFields(boolean alwaysShow);
- // toggle showing the saved wifi password in wifi form, could be a security issue.
- void setShowPassword(boolean show);
-
- //if false, disable captive portal redirection
- void setCaptivePortalEnable(boolean enabled);
-
//if false, timeout captive portal even if a STA client connected to softAP (false), suggest disabling if captiveportal is open
void setAPClientCheck(boolean enabled);
@@ -449,15 +511,6 @@ class WiFiManager
bool setHostname(const char * hostname);
bool setHostname(String hostname);
- // show erase wifi onfig button on info page, true
- void setShowInfoErase(boolean enabled);
-
- // show OTA upload button on info page
- void setShowInfoUpdate(boolean enabled);
-
- // show Info nav / info view (portal SPA); default true
- void setShowInfo(boolean enabled);
-
// set ap channel
void setWiFiAPChannel(int32_t channel);
@@ -467,18 +520,46 @@ class WiFiManager
// clean connect, always disconnect before connecting
void setCleanConnect(bool enable); // default false
- // set the webapp title, default WiFiManager
- void setTitle(String title);
+ // ---- Portal (all customization entry points use the portal* prefix) ----
+ void portalSetBrandTitle(const String& title);
+ void portalSetContextIdentityText(const String& identityText);
+ void portalSetBrandHomeIntro(const String& text);
+ void portalSetBrandLogoSvg(const String& svgMarkup);
- /**
- * Where custom parameters (`WiFiManagerParameter`) appear in the portal SPA (one placement only).
- *
- * - true (default): parameters render on the WiFi route (`#/wifi`) together with SSID/password.
- * - false: parameters render only on the Setup route (`#/setup`); they are not duplicated on WiFi.
- *
- * Bootstrap exposes the same flag as `features.paramsInWifi`.
- */
- void setParametersEmbeddedInWifi(boolean embedded);
+ void portalSetPageInfoVisible(bool visible);
+ void portalSetPageUpdateVisible(bool visible);
+ void portalSetPageSetupVisible(bool visible);
+
+ void portalSetActionEraseVisible(bool visible);
+ void portalSetActionRestartVisible(bool visible);
+ void portalSetActionExitVisible(bool visible);
+ void portalSetActionCloseCaptiveVisible(bool visible);
+ void portalSetActionBackVisible(bool visible);
+
+ void portalSetLayoutParamsLocation(PortalParamsLocation location);
+
+ void portalSetBehaviorCaptivePortalEnabled(bool enabled);
+ void portalSetBehaviorConnectOnSave(bool enabled);
+ void portalSetBehaviorExitAllowed(bool allowed);
+ void portalSetBehaviorConnectTimeoutSeconds(unsigned long seconds);
+ void portalSetBehaviorPortalTimeoutSeconds(unsigned long seconds);
+ void portalSetBehaviorAutoReconnect(bool enabled);
+ void portalSetBehaviorApClientCheck(bool enabled);
+ void portalSetBehaviorWebClientCheck(bool enabled);
+
+ void portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode mode);
+ void portalSetFieldStaticIpVisibility(PortalFieldVisibility visibility);
+ void portalSetFieldStaticDnsVisibility(PortalFieldVisibility visibility);
+
+ void portalClearParameters();
+ void portalAddInfoSection(const PortalInfoSection& section);
+ void portalClearInfoSections();
+ void portalAddHomeCard(const PortalHomeCard& card);
+ void portalClearHomeCards();
+
+ void portalAppendCss(const String& css);
+ void portalOverrideCss(const String& css);
+ void portalAppendJs(const String& js);
// get last connection result, including autoconnect and portal credential-save attempts
uint8_t getLastConxResult();
@@ -706,7 +787,7 @@ class WiFiManager
int _staShowStaticFields = 0; // ternary 1=always show static ip fields, 0=only if set, -1=never(cannot change ips via web!)
int _staShowDns = 0; // ternary 1=always show dns, 0=only if set, -1=never(cannot change dns via web!)
boolean _removeDuplicateAPs = true; // remove dup aps from wifiscan
- boolean _showPassword = false; // show or hide saved password on wifi form, might be a security issue!
+ PortalPasswordPlaceholderMode _portalPasswordPlaceholderMode = PortalPasswordPlaceholderMode::Masked;
boolean _shouldBreakAfterConfig = false; // stop configportal on save failure
boolean _enableCaptivePortal = true; // enable captive portal redirection
boolean _userpersistent = true; // users preffered persistence to restore
@@ -714,15 +795,17 @@ class WiFiManager
boolean _apClientCheck = false; // keep cp alive if ap have station
boolean _webClientCheck = true; // keep cp alive if web have client
boolean _scanDispOptions = false; // show percentage in scans not icons
- boolean _paramsInWifi = true; // if false, custom params only on #/setup (see setParametersEmbeddedInWifi)
- boolean _showInfo = true; // show Info in portal SPA
- boolean _showInfoErase = true; // info page erase button
- boolean _showInfoUpdate = true; // info page update button
- boolean _showBack = false; // show back button
boolean _enableConfigPortal = true; // FOR autoconnect - start config portal if autoconnect failed
boolean _disableConfigPortal = true; // FOR autoconnect - stop config portal if cp wifi save
String _hostname = ""; // hostname for esp8266 for dhcp, and or MDNS
- String _title = "WiFiManager"; // app title - default WiFiManager
+
+ // Grouped portal presentation / customization (see portal* setters; JSON in v2 bootstrap)
+ PortalBrandState _portalBrand;
+ PortalPageState _portalPages;
+ PortalActionState _portalActions;
+ PortalLayoutState _portalLayout;
+ PortalAssetState _portalAssets;
+ PortalStructuredExtrasState _portalStructured;
// internal options
diff --git a/lib/WiFiManager/include/WiFiManagerHandlers.h b/lib/WiFiManager/include/WiFiManagerHandlers.h
index b57f671..f0ebcda 100644
--- a/lib/WiFiManager/include/WiFiManagerHandlers.h
+++ b/lib/WiFiManager/include/WiFiManagerHandlers.h
@@ -20,8 +20,7 @@
#define WM_TEMPLATE_REGISTRY_CAPACITY 16
#endif
-// Portal UI customization is driven by WiFiManager state and helpers such as setTitle(), setShowInfo(),
-// setParametersEmbeddedInWifi(), setShowInfoUpdate(), etc., surfaced through bootstrap/API JSON — not by mutating template
+// Portal UI customization is driven by WiFiManager `portal*` APIs and JSON (/api/...) — not by mutating template
// placeholder registries.
// -----------------------------------------------------------------------------------------------
@@ -106,11 +105,14 @@ class WiFiManagerHandlers {
void appendPortalJsonStaticFields(String& json, bool& first);
void appendPortalJsonCustomParams(String& json, bool& first);
+ String composePortalStylesheet() const;
+ void appendPortalExtraInfoSectionsJson(String& json, bool& first);
+ void appendPortalExtraHomeCardsJson(String& json, bool& first);
+
void appendJsonKvItem(String& json, bool& first, const char* key, const String& label, const String& value);
void appendOneInfoItemForId(String& json, bool& first, const char* id);
void appendInfoSectionFromIds(String& json, const char* const* ids, size_t count, bool& first);
- void appendPortalUiFeatureFlagsJson(String& json);
void appendApiInfoActionsJson(String& json);
static void sendApiJson(AsyncWebServerRequest *request, int code, const String& json);
diff --git a/lib/WiFiManager/include/templates/CSS.h b/lib/WiFiManager/include/templates/CSS.h
index 6f7aaa5..20e45ca 100644
--- a/lib/WiFiManager/include/templates/CSS.h
+++ b/lib/WiFiManager/include/templates/CSS.h
@@ -70,6 +70,9 @@ const char CSS_STYLE[] PROGMEM = "");
+
+ if (css.startsWith(openTag)) {
+ css.remove(0, openTag.length());
+ }
+ if (css.endsWith(closeTag)) {
+ css.remove(css.length() - closeTag.length());
+ }
+ return css;
+}
+
+const char* portalHomeCardKindJson(PortalHomeCardKind k) {
+ switch (k) {
+ case PortalHomeCardKind::Text:
+ return "text";
+ case PortalHomeCardKind::Callout:
+ return "callout";
+ case PortalHomeCardKind::KeyValue:
+ default:
+ return "kv";
+ }
+}
+
template
AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request,
const std::shared_ptr& bundle,
@@ -73,41 +102,123 @@ AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request,
);
}
-// Shell contract (must match templates/RootShell.h — customize UI via WiFiManager APIs + bootstrap JSON, not registries):
-// %PAGE_TITLE% -> document title (from WiFiManager title state)
-// %STYLES% -> embedded portal CSS
-// %BOOTSTRAP_JSON% -> initial SPA runtime payload
-// %PORTAL_APP_JS% -> embedded SPA source
+// Shell contract (must match templates/RootShell.h — customize UI via WiFiManager portal* APIs + bootstrap JSON, not registries):
+// %PAGE_TITLE% -> document title (from WiFiManager title state)
+// %STYLES% -> portal CSS (embedded default + portalAppendCss / portalOverrideCss)
+// %BOOTSTRAP_JSON% -> initial SPA runtime payload
+// %PORTAL_APP_JS% -> embedded SPA source
+// %PORTAL_APPEND_JS% -> consumer hook script (portalAppendJs)
struct PortalShellRenderBundle {
String bootstrapJson;
String pageTitleStatic;
+ String stylesStatic;
+ String appendJsStatic;
PlaceholderRegistry registry;
TemplateContext context;
- DynamicTemplateDescriptor bootstrapDescriptor;
- DynamicTemplateDescriptor pageTitleDescriptor;
+ DynamicDataDescriptor bootstrapDescriptor;
+ DynamicDataDescriptor pageTitleDescriptor;
+ DynamicDataDescriptor stylesDescriptor;
+ DynamicDataDescriptor appendJsDescriptor;
PortalShellRenderBundle()
- : registry(WM_TEMPLATE_REGISTRY_CAPACITY), bootstrapDescriptor{}, pageTitleDescriptor{} {}
+ : registry(WM_TEMPLATE_REGISTRY_CAPACITY),
+ bootstrapDescriptor{},
+ pageTitleDescriptor{},
+ stylesDescriptor{},
+ appendJsDescriptor{} {}
};
-void populatePortalShellBundle(PortalShellRenderBundle& bundle,
- const String& pageTitle,
- const String& bootstrapJson) {
- bundle.pageTitleStatic = pageTitle;
- bundle.bootstrapJson = bootstrapJson;
- bundle.registry.registerProgmemData("%STYLES%", CSS_STYLE);
- bundle.registry.registerProgmemData("%PORTAL_APP_JS%", PORTAL_APP_JS);
- configureDynamicStringDescriptor(bundle.pageTitleDescriptor, bundle.pageTitleStatic);
- bundle.registry.registerDynamicTemplate("%PAGE_TITLE%", &bundle.pageTitleDescriptor);
- configureDynamicStringDescriptor(bundle.bootstrapDescriptor, bundle.bootstrapJson);
- bundle.registry.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bundle.bootstrapDescriptor);
-}
-
} // namespace
WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {}
+String WiFiManagerHandlers::composePortalStylesheet() const {
+ String css;
+ if (_wm != nullptr && _wm->_portalAssets.overriddenCss.length() > 0) {
+ css = _wm->_portalAssets.overriddenCss;
+ } else {
+ css = stripStyleWrapper(readProgmemString(CSS_STYLE));
+ }
+
+ if (_wm != nullptr && _wm->_portalAssets.appendedCss.length() > 0) {
+ css += _wm->_portalAssets.appendedCss;
+ }
+
+ String out = F("");
+ return out;
+}
+
+void WiFiManagerHandlers::appendPortalExtraInfoSectionsJson(String& json, bool& first) {
+ if (_wm == nullptr) {
+ return;
+ }
+ for (const auto& sec : _wm->_portalStructured.infoSections) {
+ if (!first) {
+ json += F(",");
+ }
+ first = false;
+ json += F("{\"id\":\"");
+ jsonAppendEscaped(json, sec.id);
+ json += F("\",\"title\":\"");
+ jsonAppendEscaped(json, sec.title);
+ json += F("\",\"items\":[");
+ bool firstItem = true;
+ for (const auto& it : sec.items) {
+ if (!firstItem) {
+ json += F(",");
+ }
+ firstItem = false;
+ json += F("{\"key\":\"");
+ jsonAppendEscaped(json, it.key);
+ json += F("\",\"label\":\"");
+ jsonAppendEscaped(json, it.label);
+ json += F("\",\"value\":\"");
+ jsonAppendEscaped(json, it.value);
+ json += F("\"}");
+ }
+ json += F("]}");
+ }
+}
+
+void WiFiManagerHandlers::appendPortalExtraHomeCardsJson(String& json, bool& first) {
+ if (_wm == nullptr) {
+ return;
+ }
+ for (const auto& card : _wm->_portalStructured.homeCards) {
+ if (!first) {
+ json += F(",");
+ }
+ first = false;
+ json += F("{\"id\":\"");
+ jsonAppendEscaped(json, card.id);
+ json += F("\",\"title\":\"");
+ jsonAppendEscaped(json, card.title);
+ json += F("\",\"kind\":\"");
+ json += portalHomeCardKindJson(card.kind);
+ json += F("\",\"text\":\"");
+ jsonAppendEscaped(json, card.text);
+ json += F("\",\"items\":[");
+ bool firstItem = true;
+ for (const auto& it : card.items) {
+ if (!firstItem) {
+ json += F(",");
+ }
+ firstItem = false;
+ json += F("{\"key\":\"");
+ jsonAppendEscaped(json, it.key);
+ json += F("\",\"label\":\"");
+ jsonAppendEscaped(json, it.label);
+ json += F("\",\"value\":\"");
+ jsonAppendEscaped(json, it.value);
+ json += F("\"}");
+ }
+ json += F("]}");
+ }
+}
+
void WiFiManagerHandlers::collectVisibleScanResults(std::vector& networks) {
networks.clear();
networks.reserve(_wm->_scanResultsCache.size());
@@ -197,7 +308,7 @@ void WiFiManagerHandlers::appendPortalJsonCustomParams(String& json, bool& first
first = false;
if (p->getID() != nullptr) {
String pid = String(p->getID());
- json += F("{\"name\":\"");
+ json += F("{\"kind\":\"field\",\"name\":\"");
jsonAppendEscaped(json, pid);
json += F("\",\"id\":\"");
jsonAppendEscaped(json, pid);
@@ -219,7 +330,7 @@ void WiFiManagerHandlers::appendPortalJsonCustomParams(String& json, bool& first
}
json += F("}");
} else {
- json += F("{\"html\":\"");
+ json += F("{\"kind\":\"html\",\"html\":\"");
jsonAppendEscaped(json, String(p->getCustomHTML()));
json += F("}");
}
@@ -560,9 +671,25 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) {
// - Populate one request-scoped PlaceholderRegistry with shell defaults + request payloads.
// - Render WM_ROOT_SHELL_TEMPLATE.
auto bundle = std::make_shared();
- populatePortalShellBundle(*bundle,
- _wm ? _wm->_title : String(),
- buildPortalBootstrapJson());
+ const bool hasCustomStyles = _wm != nullptr &&
+ (_wm->_portalAssets.overriddenCss.length() > 0 || _wm->_portalAssets.appendedCss.length() > 0);
+ bundle->pageTitleStatic = _wm ? _wm->_portalBrand.title : String();
+ bundle->bootstrapJson = buildPortalBootstrapJson();
+ bundle->appendJsStatic = _wm ? _wm->_portalAssets.appendedJs : String();
+ bundle->registry.registerProgmemData("%PORTAL_APP_JS%", PORTAL_APP_JS);
+ if (hasCustomStyles) {
+ bundle->stylesStatic = composePortalStylesheet();
+ configureDynamicStringDescriptor(bundle->stylesDescriptor, bundle->stylesStatic);
+ bundle->registry.registerDynamicData("%STYLES%", &bundle->stylesDescriptor);
+ } else {
+ bundle->registry.registerProgmemData("%STYLES%", CSS_STYLE);
+ }
+ configureDynamicStringDescriptor(bundle->appendJsDescriptor, bundle->appendJsStatic);
+ bundle->registry.registerDynamicData("%PORTAL_APPEND_JS%", &bundle->appendJsDescriptor);
+ configureDynamicStringDescriptor(bundle->pageTitleDescriptor, bundle->pageTitleStatic);
+ bundle->registry.registerDynamicData("%PAGE_TITLE%", &bundle->pageTitleDescriptor);
+ configureDynamicStringDescriptor(bundle->bootstrapDescriptor, bundle->bootstrapJson);
+ bundle->registry.registerDynamicData("%BOOTSTRAP_JSON%", &bundle->bootstrapDescriptor);
request->send(beginTemplateResponse(request, bundle, WM_ROOT_SHELL_TEMPLATE));
}
@@ -634,7 +761,7 @@ void WiFiManagerHandlers::applyWifiAndParamsFromRequest(AsyncWebServerRequest *r
_wm->_presavewificallback();
}
- if (_wm->_paramsInWifi) {
+ if (_wm->_portalLayout.paramsOnWifiPage) {
doParamSave(requestArgs);
}
}
@@ -821,38 +948,20 @@ void WiFiManagerHandlers::sendApiJson(AsyncWebServerRequest *request, int code,
request->send(response);
}
-void WiFiManagerHandlers::appendPortalUiFeatureFlagsJson(String& json) {
- const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
- json += F("\"showInfo\":");
- json += _wm->_showInfo ? F("true") : F("false");
- json += F(",\"showUpdate\":");
- json += _wm->_showInfoUpdate ? F("true") : F("false");
- json += F(",\"showErase\":");
- json += _wm->_showInfoErase ? F("true") : F("false");
- json += F(",\"paramsInWifi\":");
- json += _wm->_paramsInWifi ? F("true") : F("false");
- json += F(",\"showRestart\":");
- json += portalRunning ? F("true") : F("false");
- json += F(",\"showExitPortal\":");
- json += (_wm->_allowExit && portalRunning) ? F("true") : F("false");
- json += F(",\"showCloseCaptive\":");
- json += (_wm->_enableCaptivePortal && _wm->configPortalActive) ? F("true") : F("false");
-}
-
void WiFiManagerHandlers::appendApiInfoActionsJson(String& json) {
const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
json += F("\"showUpdate\":");
- json += _wm->_showInfoUpdate ? F("true") : F("false");
+ json += _wm->_portalPages.updateVisible ? F("true") : F("false");
json += F(",\"showErase\":");
- json += _wm->_showInfoErase ? F("true") : F("false");
+ json += _wm->_portalActions.eraseVisible ? F("true") : F("false");
json += F(",\"showBack\":");
- json += _wm->_showBack ? F("true") : F("false");
+ json += _wm->_portalActions.backVisible ? F("true") : F("false");
json += F(",\"showRestart\":");
- json += portalRunning ? F("true") : F("false");
+ json += (_wm->_portalActions.restartVisible && portalRunning) ? F("true") : F("false");
json += F(",\"showExitPortal\":");
- json += (_wm->_allowExit && portalRunning) ? F("true") : F("false");
+ json += (_wm->_portalActions.exitVisible && _wm->_allowExit && portalRunning) ? F("true") : F("false");
json += F(",\"showCloseCaptive\":");
- json += (_wm->_enableCaptivePortal && _wm->configPortalActive) ? F("true") : F("false");
+ json += (_wm->_portalActions.closeCaptiveVisible && _wm->_enableCaptivePortal && _wm->configPortalActive) ? F("true") : F("false");
}
String WiFiManagerHandlers::buildPortalBootstrapJson() {
@@ -882,38 +991,87 @@ String WiFiManagerHandlers::buildPortalBootstrapJson() {
stateStr = "idle";
break;
}
+
+ const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
String json;
- reservePage(json, 512);
- json += F("{");
+ reservePage(json, 1200);
+ json += F("{\"contractVersion\":2");
+ json += F(",\"brand\":{");
json += F("\"title\":\"");
- jsonAppendEscaped(json, _wm->_title);
- json += F("\",\"subtitle\":\"");
+ jsonAppendEscaped(json, _wm->_portalBrand.title);
+ json += F("\",\"homeIntro\":\"");
+ jsonAppendEscaped(json, _wm->_portalBrand.homeIntro);
+ json += F("\",\"logoSvg\":\"");
+ jsonAppendEscaped(json, _wm->_portalBrand.logoSvg);
+ json += F("\"}");
+ json += F(",\"context\":{");
+ json += F("\"portalActive\":");
+ json += portalRunning ? F("true") : F("false");
+ json += F(",\"portalTimeoutSecondsRemaining\":");
{
- String sub;
- if (_wm->configPortalActive) {
- sub = _wm->_apName;
- } else {
- sub = _wm->getWiFiHostname() + " - " + WiFi.localIP().toString();
+ unsigned long timeoutRemainingSeconds = 0;
+ if (_wm->configPortalActive && _wm->_configPortalTimeout > 0) {
+ const unsigned long now = millis();
+ const unsigned long timeoutAt = _wm->_configPortalStart + _wm->_configPortalTimeout;
+ if (timeoutAt > now) {
+ timeoutRemainingSeconds = (timeoutAt - now + 999UL) / 1000UL;
+ }
}
- jsonAppendEscaped(json, sub);
+ json += String(timeoutRemainingSeconds);
}
- json += F("\",\"portalActive\":");
- json += _wm->configPortalActive ? F("true") : F("false");
- json += F(",\"features\":{");
- appendPortalUiFeatureFlagsJson(json);
- json += F("},\"showBack\":");
- json += _wm->_showBack ? F("true") : F("false");
- json += F(",\"scan\":{\"state\":\"");
- json += stateStr;
- json += F("\",\"count\":");
- json += String((unsigned int)networks.size());
- json += F("},\"initialStatus\":\"");
+ json += F(",\"identityText\":\"");
+ {
+ String idText;
+ if (_wm->_portalBrand.identityTextOverride.length() > 0) {
+ idText = _wm->_portalBrand.identityTextOverride;
+ } else if (_wm->configPortalActive) {
+ idText = _wm->_apName;
+ } else {
+ idText = _wm->getWiFiHostname() + " - " + WiFi.localIP().toString();
+ }
+ jsonAppendEscaped(json, idText);
+ }
+ json += F("\",\"statusSummary\":\"");
{
String st;
buildPlainStatusSummary(st);
jsonAppendEscaped(json, st);
}
+ json += F("\",\"scan\":{\"state\":\"");
+ json += stateStr;
+ json += F("\",\"count\":");
+ json += String((unsigned int)networks.size());
+ json += F("}}");
+ json += F(",\"pages\":{");
+ json += F("\"wifi\":{\"visible\":true}");
+ json += F(",\"setup\":{\"visible\":");
+ json += (!_wm->_portalLayout.paramsOnWifiPage && _wm->_portalPages.setupVisible) ? F("true") : F("false");
+ json += F("}");
+ json += F(",\"info\":{\"visible\":");
+ json += _wm->_portalPages.infoVisible ? F("true") : F("false");
+ json += F("}");
+ json += F(",\"update\":{\"visible\":");
+ json += _wm->_portalPages.updateVisible ? F("true") : F("false");
+ json += F("}}");
+ json += F(",\"actions\":{");
+ json += F("\"erase\":{\"visible\":");
+ json += _wm->_portalActions.eraseVisible ? F("true") : F("false");
+ json += F("},\"restart\":{\"visible\":");
+ json += (_wm->_portalActions.restartVisible && portalRunning) ? F("true") : F("false");
+ json += F("},\"exitPortal\":{\"visible\":");
+ json += (_wm->_portalActions.exitVisible && _wm->_allowExit && portalRunning) ? F("true") : F("false");
+ json += F("},\"closeCaptive\":{\"visible\":");
+ json += (_wm->_portalActions.closeCaptiveVisible && _wm->_enableCaptivePortal && _wm->configPortalActive) ? F("true") : F("false");
+ json += F("},\"back\":{\"visible\":");
+ json += _wm->_portalActions.backVisible ? F("true") : F("false");
+ json += F("}}");
+ json += F(",\"layout\":{\"paramsLocation\":\"");
+ json += _wm->_portalLayout.paramsOnWifiPage ? F("wifi") : F("setup");
json += F("\"}");
+ bool first = true;
+ json += F(",\"extraHomeCards\":[");
+ appendPortalExtraHomeCardsJson(json, first);
+ json += F("]}");
return json;
}
@@ -1019,10 +1177,19 @@ void WiFiManagerHandlers::handleApiWifiScan(AsyncWebServerRequest *request) {
String WiFiManagerHandlers::buildApiWifiMetaJson() {
String ssidPlaceholder = _wm->WiFi_SSID();
String passwordPlaceholder = "";
- if (_wm->_showPassword) {
- passwordPlaceholder = _wm->WiFi_psk();
- } else if (_wm->WiFi_psk() != "") {
- passwordPlaceholder = F("********");
+ switch (_wm->_portalPasswordPlaceholderMode) {
+ case PortalPasswordPlaceholderMode::Actual:
+ passwordPlaceholder = _wm->WiFi_psk();
+ break;
+ case PortalPasswordPlaceholderMode::Masked:
+ if (_wm->WiFi_psk() != "") {
+ passwordPlaceholder = F("********");
+ }
+ break;
+ case PortalPasswordPlaceholderMode::Hidden:
+ default:
+ passwordPlaceholder = "";
+ break;
}
String json = F("{\"wifiFields\":[");
@@ -1035,11 +1202,11 @@ String WiFiManagerHandlers::buildApiWifiMetaJson() {
appendPortalJsonStaticFields(json, first);
json += F("],\"params\":[");
first = true;
- if (_wm->_paramsInWifi && _wm->getParametersCount() > 0) {
+ if (_wm->_portalLayout.paramsOnWifiPage && _wm->getParametersCount() > 0) {
appendPortalJsonCustomParams(json, first);
}
json += F("],\"actions\":{\"canRefreshScan\":true,\"showBack\":");
- json += _wm->_showBack ? F("true") : F("false");
+ json += _wm->_portalActions.backVisible ? F("true") : F("false");
json += F("}}");
return json;
}
@@ -1096,7 +1263,7 @@ String WiFiManagerHandlers::buildApiParamsGetJson() {
bool first = true;
appendPortalJsonCustomParams(json, first);
json += F("],\"actions\":{\"showBack\":");
- json += _wm->_showBack ? F("true") : F("false");
+ json += _wm->_portalActions.backVisible ? F("true") : F("false");
json += F("}}");
return json;
}
@@ -1156,6 +1323,9 @@ String WiFiManagerHandlers::buildApiInfoJson() {
first = true;
static const char *const aboutIds[] = {"aboutver", "aboutarduinover", "aboutsdkver", "aboutdate"};
appendInfoSectionFromIds(json, aboutIds, sizeof(aboutIds) / sizeof(aboutIds[0]), first);
+ json += F("],\"extraSections\":[");
+ first = true;
+ appendPortalExtraInfoSectionsJson(json, first);
json += F("],\"actions\":{");
appendApiInfoActionsJson(json);
json += F("}}");
diff --git a/test/test_wifimanager/test_main.cpp b/test/test_wifimanager/test_main.cpp
index 9813457..3a6226f 100644
--- a/test/test_wifimanager/test_main.cpp
+++ b/test/test_wifimanager/test_main.cpp
@@ -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),
diff --git a/test/test_wifimanager/test_main.h b/test/test_wifimanager/test_main.h
index b283072..d4c8262 100644
--- a/test/test_wifimanager/test_main.h
+++ b/test/test_wifimanager/test_main.h
@@ -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();
diff --git a/test/test_wifimanager/tests/test_advanced_options.cpp b/test/test_wifimanager/tests/test_advanced_options.cpp
index 8b69382..6962483 100644
--- a/test/test_wifimanager/tests/test_advanced_options.cpp
+++ b/test/test_wifimanager/tests/test_advanced_options.cpp
@@ -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");
diff --git a/test/test_wifimanager/tests/test_api_json_and_captive.cpp b/test/test_wifimanager/tests/test_api_json_and_captive.cpp
index 092667b..762ae77 100644
--- a/test/test_wifimanager/tests/test_api_json_and_captive.cpp
+++ b/test/test_wifimanager/tests/test_api_json_and_captive.cpp
@@ -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("");
+ 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");
diff --git a/test/test_wifimanager/tests/test_basic.cpp b/test/test_wifimanager/tests/test_basic.cpp
index aef1ec8..1494f50 100644
--- a/test/test_wifimanager/tests/test_basic.cpp
+++ b/test/test_wifimanager/tests/test_basic.cpp
@@ -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");
}
diff --git a/test/test_wifimanager/tests/test_configuration.cpp b/test/test_wifimanager/tests/test_configuration.cpp
index ac0d550..48ebf33 100644
--- a/test/test_wifimanager/tests/test_configuration.cpp
+++ b/test/test_wifimanager/tests/test_configuration.cpp
@@ -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");
}
diff --git a/test/test_wifimanager/tests/test_error_conditions.cpp b/test/test_wifimanager/tests/test_error_conditions.cpp
index d457237..587a32e 100644
--- a/test/test_wifimanager/tests/test_error_conditions.cpp
+++ b/test/test_wifimanager/tests/test_error_conditions.cpp
@@ -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());
diff --git a/test/test_wifimanager/tests/test_integration.cpp b/test/test_wifimanager/tests/test_integration.cpp
index ddaf56f..eed5e29 100644
--- a/test/test_wifimanager/tests/test_integration.cpp
+++ b/test/test_wifimanager/tests/test_integration.cpp
@@ -70,7 +70,7 @@ void test_parameter_add_and_retrieve() {
// 1. Add parameters
WiFiManagerParameter param("server", "Server", "default", 40);
- wm.addParameter(¶m);
+ wm.portalAddParameter(¶m);
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");
diff --git a/test/test_wifimanager/tests/test_parameter_values.cpp b/test/test_wifimanager/tests/test_parameter_values.cpp
index 33a335e..a0efd2a 100644
--- a/test/test_wifimanager/tests/test_parameter_values.cpp
+++ b/test/test_wifimanager/tests/test_parameter_values.cpp
@@ -44,7 +44,7 @@ void test_parameter_value_persistence() {
WiFiManager wm;
WiFiManagerParameter param("server", "Server", "default", 40);
- wm.addParameter(¶m);
+ wm.portalAddParameter(¶m);
// 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);
diff --git a/test/test_wifimanager/tests/test_parameters.cpp b/test/test_wifimanager/tests/test_parameters.cpp
index bb676f5..b7fb2e5 100644
--- a/test/test_wifimanager/tests/test_parameters.cpp
+++ b/test/test_wifimanager/tests/test_parameters.cpp
@@ -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(¶m1);
- bool result2 = wm.addParameter(¶m2);
+ bool result1 = wm.portalAddParameter(¶m1);
+ bool result2 = wm.portalAddParameter(¶m2);
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(¶m1);
- wm.addParameter(¶m2);
- wm.addParameter(¶m3);
+ wm.portalAddParameter(¶m1);
+ wm.portalAddParameter(¶m2);
+ wm.portalAddParameter(¶m3);
// 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());
diff --git a/test/test_wifimanager/tests/test_root_render_lifecycle.cpp b/test/test_wifimanager/tests/test_root_render_lifecycle.cpp
index d1f8a25..d664d70 100644
--- a/test/test_wifimanager/tests/test_root_render_lifecycle.cpp
+++ b/test/test_wifimanager/tests/test_root_render_lifecycle.cpp
@@ -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(" ");
+ 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\":\" \"")));
+ 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;
diff --git a/test/test_wifimanager/tests/test_stress.cpp b/test/test_wifimanager/tests/test_stress.cpp
index 6642ef9..28e04f2 100644
--- a/test/test_wifimanager/tests/test_stress.cpp
+++ b/test/test_wifimanager/tests/test_stress.cpp
@@ -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());
diff --git a/test/test_wifimanager/tests/test_template_rendering.cpp b/test/test_wifimanager/tests/test_template_rendering.cpp
index 7af1cea..b128be5 100644
--- a/test/test_wifimanager/tests/test_template_rendering.cpp
+++ b/test/test_wifimanager/tests/test_template_rendering.cpp
@@ -2,6 +2,7 @@
#include
#include
#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(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("");
+ 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(""));
+ TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Config ESP "));
+ TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("{\"title\":\"Test\"}"));
+
+ Serial.println("[TEST] Dynamic styles placeholder percent rendering test completed successfully");
+}
diff --git a/test/test_wifimanager/tests/test_wifi_scanning.cpp b/test/test_wifimanager/tests/test_wifi_scanning.cpp
index 6b70848..2327560 100644
--- a/test/test_wifimanager/tests/test_wifi_scanning.cpp
+++ b/test/test_wifimanager/tests/test_wifi_scanning.cpp
@@ -57,7 +57,7 @@ void test_scan_status_checking() {
TEST_ASSERT_EQUAL(static_cast(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");