mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-04 02:48:13 +10:00
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:
@@ -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(
|
||||
"<svg viewBox='0 0 24 24' aria-hidden='true'>"
|
||||
"<path d='M12 2L4 12h5l-1 10 8-10h-5z'></path>"
|
||||
"</svg>"
|
||||
);
|
||||
|
||||
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(
|
||||
"<div class='wm-callout wm-callout--info'>"
|
||||
"<p>GPS options below apply only when a GPS module is connected.</p>"
|
||||
"</div>"
|
||||
);
|
||||
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**.
|
||||
|
||||
@@ -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<PortalKeyValueItem> items;
|
||||
};
|
||||
|
||||
struct PortalHomeCard {
|
||||
String id;
|
||||
String title;
|
||||
PortalHomeCardKind kind = PortalHomeCardKind::KeyValue;
|
||||
String text;
|
||||
std::vector<PortalKeyValueItem> 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<PortalInfoSection> infoSections;
|
||||
std::vector<PortalHomeCard> 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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -70,6 +70,9 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
".wm-hero{margin-bottom:20px}"
|
||||
".wm-hero-title{font-size:1.65rem;line-height:1.2;margin-bottom:.25em}"
|
||||
".wm-hero-sub{font-size:1rem;color:var(--wm-muted);font-weight:500;margin:0}"
|
||||
".wm-brand-logo{margin-bottom:12px;line-height:0}"
|
||||
".wm-brand-logo svg{display:block;max-width:160px;height:auto}"
|
||||
".wm-hero-intro{font-size:.95rem;color:var(--wm-muted);margin:.5em 0 0;line-height:1.45;max-width:42ch}"
|
||||
".wm-page-head{margin-bottom:16px}"
|
||||
".wm-page-head h1{font-size:1.45rem}"
|
||||
".wm-page-desc{font-size:.95rem;color:var(--wm-muted);margin:.35em 0 0}"
|
||||
@@ -85,7 +88,7 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
"background:rgba(14,165,233,.06);margin-bottom:16px}"
|
||||
"body.invert .wm-callout{background:rgba(56,189,248,.08)}"
|
||||
".wm-callout--info{border-left:4px solid var(--wm-brand)}"
|
||||
".wm-status{margin:0;font-size:.9rem}"
|
||||
".wm-status{margin:0 0 10px;font-size:.9rem;font-weight:600;color:var(--wm-brand)}"
|
||||
/* Form fields */
|
||||
".wm-field{margin-bottom:14px;text-align:left}"
|
||||
".wm-field:last-child{margin-bottom:0}"
|
||||
@@ -130,10 +133,13 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
".wm-scan-row:hover{background:rgba(14,165,233,.1)}"
|
||||
"body.invert .wm-scan-row:hover{background:rgba(56,189,248,.12)}"
|
||||
".wm-scan-ssid{flex:1;min-width:0;font-weight:600;font-size:.95rem;text-align:left;word-break:break-word}"
|
||||
".wm-scan-signal{display:flex;align-items:center;gap:6px;flex-shrink:0;font-size:.8rem;color:var(--wm-muted);font-weight:600}"
|
||||
".wm-scan-meta{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-shrink:0;min-width:78px}"
|
||||
".wm-scan-signal{display:block;flex-shrink:0;font-size:.8rem;color:var(--wm-muted);font-weight:600;min-width:38px;text-align:right}"
|
||||
".wm-scan-lock,.wm-scan-bars{display:block;flex:0 0 16px}"
|
||||
// quality icons
|
||||
".q{height:16px;width:16px;margin:0;padding:0;flex-shrink:0}.q.q-0:after{background-position-x:0}.q.q-1:after{background-position-x:-16px}.q.q-2:after{background-position-x:-32px}.q.q-3:after{background-position-x:-48px}.q.q-4:after{background-position-x:-64px}.q.l:before{background-position-x:-80px;padding-right:5px}.ql .q{float:left}.q:after,.q:before{content:'';width:16px;height:16px;display:inline-block;background-repeat:no-repeat;background-position:16px 0;"
|
||||
".q{position:relative;height:16px;width:16px;margin:0;padding:0;flex-shrink:0}.q.q-0:after{background-position-x:0}.q.q-1:after{background-position-x:-16px}.q.q-2:after{background-position-x:-32px}.q.q-3:after{background-position-x:-48px}.q.q-4:after{background-position-x:-64px}.q.l:before{background-position-x:-80px}.ql .q{float:left}.q:after,.q:before{content:'';position:absolute;left:0;top:0;width:16px;height:16px;display:block;background-repeat:no-repeat;background-position:16px 0;"
|
||||
"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAQCAMAAADeZIrLAAAAJFBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHJj5lAAAAC3RSTlMAIjN3iJmqu8zd7vF8pzcAAABsSURBVHja7Y1BCsAwCASNSVo3/v+/BUEiXnIoXkoX5jAQMxTHzK9cVSnvDxwD8bFx8PhZ9q8FmghXBhqA1faxk92PsxvRc2CCCFdhQCbRkLoAQ3q/wWUBqG35ZxtVzW4Ed6LngPyBU2CobdIDQ5oPWI5nCUwAAAAASUVORK5CYII=');}"
|
||||
".wm-scan-lock:after,.wm-scan-bars:before{content:none}"
|
||||
"@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.q:before,.q:after {"
|
||||
"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAAgCAMAAACfM+KhAAAALVBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAOrOgAAAADnRSTlMAESIzRGZ3iJmqu8zd7gKjCLQAAACmSURBVHgB7dDBCoMwEEXRmKlVY3L//3NLhyzqIqSUggy8uxnhCR5Mo8xLt+14aZ7wwgsvvPA/ofv9+44334UXXngvb6XsFhO/VoC2RsSv9J7x8BnYLW+AjT56ud/uePMdb7IP8Bsc/e7h8Cfk912ghsNXWPpDC4hvN+D1560A1QPORyh84VKLjjdvfPFm++i9EWq0348XXnjhhT+4dIbCW+WjZim9AKk4UZMnnCEuAAAAAElFTkSuQmCC');"
|
||||
"background-size: 95px 16px;}}"
|
||||
|
||||
@@ -24,6 +24,7 @@ function esc(t){if(!t)return'';return String(t).replace(/[&<>"']/g,function(c){r
|
||||
|
||||
// --- Toast / dialog ---
|
||||
var _wmToastTimer=null;
|
||||
var _wmPortalTimeoutTimer=null;
|
||||
function showToast(msg,isErr){
|
||||
var el=$('wm-toast');
|
||||
if(!el)return;
|
||||
@@ -72,9 +73,17 @@ function bindFormSubmitHandlers(){
|
||||
}
|
||||
|
||||
// --- View shell ---
|
||||
function stopPortalTimeoutCountdown(){
|
||||
if(_wmPortalTimeoutTimer){
|
||||
clearInterval(_wmPortalTimeoutTimer);
|
||||
_wmPortalTimeoutTimer=null;
|
||||
}
|
||||
}
|
||||
|
||||
function setView(html){
|
||||
var a=$('app');
|
||||
if(!a)return;
|
||||
stopPortalTimeoutCountdown();
|
||||
stopWifiScanPolling();
|
||||
stopWifiConnectPolling();
|
||||
a.innerHTML="<div class='wm-layout'>"+html+"</div>";
|
||||
@@ -82,10 +91,35 @@ function setView(html){
|
||||
bindFormSubmitHandlers();
|
||||
}
|
||||
|
||||
function formatDurationSeconds(totalSeconds){
|
||||
totalSeconds=Math.max(0,Math.floor(totalSeconds||0));
|
||||
var hours=Math.floor(totalSeconds/3600);
|
||||
var minutes=Math.floor((totalSeconds%3600)/60);
|
||||
var seconds=totalSeconds%60;
|
||||
if(hours>0){
|
||||
return hours+":"+String(minutes).padStart(2,'0')+":"+String(seconds).padStart(2,'0');
|
||||
}
|
||||
return minutes+":"+String(seconds).padStart(2,'0');
|
||||
}
|
||||
|
||||
function startPortalTimeoutCountdown(initialSeconds){
|
||||
var el=$('wm-portal-timeout');
|
||||
var remaining=Math.max(0,Math.floor(initialSeconds||0));
|
||||
if(!el||remaining<=0)return;
|
||||
function render(){
|
||||
if(!el)return;
|
||||
el.textContent='Captive portal timeout: '+formatDurationSeconds(remaining);
|
||||
}
|
||||
render();
|
||||
_wmPortalTimeoutTimer=setInterval(function(){
|
||||
remaining=Math.max(0,remaining-1);
|
||||
render();
|
||||
if(remaining<=0)stopPortalTimeoutCountdown();
|
||||
},1000);
|
||||
}
|
||||
|
||||
// --- Field render (WiFi / params meta) ---
|
||||
function renderMetaField(f){
|
||||
if(!f)return'';
|
||||
if(f.html)return f.html;
|
||||
function renderStandardField(f){
|
||||
var id=f.id||f.name||'';
|
||||
var nm=f.name||f.id||'';
|
||||
var domId=(id==='s')?'wm-s':(id==='p')?'wm-p':('wm-f-'+id);
|
||||
@@ -102,6 +136,13 @@ function renderMetaField(f){
|
||||
return h;
|
||||
}
|
||||
|
||||
function renderMetaField(f){
|
||||
if(!f)return'';
|
||||
if(f.kind==='html')return f.html||'';
|
||||
if(f.kind==='field' || !f.kind)return renderStandardField(f);
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderFieldList(arr){
|
||||
if(!arr||!arr.length)return'';
|
||||
var h='',i;
|
||||
@@ -111,43 +152,78 @@ function renderFieldList(arr){
|
||||
|
||||
function navBar(active){
|
||||
active=active||'home';
|
||||
var f=boot.features||{};
|
||||
var pages=boot.pages||{};
|
||||
var layout=boot.layout||{};
|
||||
var actions=boot.actions||{};
|
||||
var pinWifi=(layout.paramsLocation||'wifi')==='wifi';
|
||||
var ps=pages.setup||{};
|
||||
var pi=pages.info||{};
|
||||
var pu=pages.update||{};
|
||||
var h="<nav class='wm-nav' aria-label='Configuration'>";
|
||||
h+="<a class='wm-nav-link"+(active==='home'?' wm-nav-link--active':'')+"' href='#/'>Home</a>";
|
||||
h+="<a class='wm-nav-link"+(active==='wifi'?' wm-nav-link--active':'')+"' href='#/wifi'>WiFi</a>";
|
||||
if(!f.paramsInWifi){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>Setup</a>";}
|
||||
if(f.showInfo!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>Info</a>";}
|
||||
if(f.showUpdate){h+="<a class='wm-nav-link"+(active==='update'?' wm-nav-link--active':'')+"' href='#/update'>Update</a>";}
|
||||
if(boot.showBack){h+="<a href='#' class='wm-nav-link' id='wm-nav-back'>Back</a>";}
|
||||
if(!pinWifi && ps.visible!==false){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>Setup</a>";}
|
||||
if(pi.visible!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>Info</a>";}
|
||||
if(pu.visible){h+="<a class='wm-nav-link"+(active==='update'?' wm-nav-link--active':'')+"' href='#/update'>Update</a>";}
|
||||
if(actions.back&&actions.back.visible){h+="<a href='#' class='wm-nav-link' id='wm-nav-back'>Back</a>";}
|
||||
h+="</nav>";
|
||||
return h;
|
||||
}
|
||||
|
||||
function deviceActionsHtml(){
|
||||
var f=boot.features||{};
|
||||
var actions=boot.actions||{};
|
||||
var h='', ok=false;
|
||||
if(f.showRestart){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>Restart device</button>";ok=true;}
|
||||
if(f.showExitPortal){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-exit-btn'>Exit portal</button>";ok=true;}
|
||||
if(f.showCloseCaptive){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-close-captive-btn'>Stop captive portal</button>";ok=true;}
|
||||
if(actions.restart&&actions.restart.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>Restart device</button>";ok=true;}
|
||||
if(actions.exitPortal&&actions.exitPortal.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-exit-btn'>Exit portal</button>";ok=true;}
|
||||
if(actions.closeCaptive&&actions.closeCaptive.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-close-captive-btn'>Stop captive portal</button>";ok=true;}
|
||||
if(!ok)return'';
|
||||
return "<div class='wm-card'><h2 class='wm-card-title'>Device</h2><div class='wm-btn-group'>"+h+"</div></div>";
|
||||
}
|
||||
|
||||
function renderExtraHomeCards(cards){
|
||||
if(!cards||!cards.length)return'';
|
||||
var out='',i,c,j,k,cls;
|
||||
for(i=0;i<cards.length;i++){
|
||||
c=cards[i];
|
||||
if(!c)continue;
|
||||
out+="<div class='wm-card'>";
|
||||
if(c.title){out+="<h2 class='wm-card-title'>"+esc(c.title)+"</h2>";}
|
||||
if(c.kind==='text'||c.kind==='callout'){
|
||||
cls=c.kind==='callout'?'wm-callout wm-callout--info':'wm-lead';
|
||||
out+="<p class='"+cls+"'>"+esc(c.text||'')+"</p>";
|
||||
}else{
|
||||
out+="<dl class='wm-kv'>";
|
||||
if(c.items){for(j=0;j<c.items.length;j++){k=c.items[j];out+="<dt>"+esc(k.label)+"</dt><dd>"+esc(k.value)+"</dd>";}}
|
||||
out+="</dl>";
|
||||
}
|
||||
out+="</div>";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function viewHome(){
|
||||
var sub=esc(boot.subtitle||'');
|
||||
var t=esc(boot.title||'WiFiManager');
|
||||
var brand=boot.brand||{};
|
||||
var ctx=boot.context||{};
|
||||
var t=esc(brand.title||'WiFiManager');
|
||||
var ident=ctx.identityText?esc(ctx.identityText):'';
|
||||
var summary=ctx.statusSummary||'';
|
||||
var timeoutSeconds=ctx.portalTimeoutSecondsRemaining||0;
|
||||
var html=navBar('home');
|
||||
html+="<header class='wm-hero'><h1 class='wm-hero-title'>"+t+"</h1>";
|
||||
if(sub)html+="<p class='wm-hero-sub'>"+sub+"</p>";
|
||||
html+="<header class='wm-hero'>";
|
||||
if(brand.logoSvg){html+="<div class='wm-brand-logo'>"+brand.logoSvg+"</div>";}
|
||||
html+="<h1 class='wm-hero-title'>"+t+"</h1>";
|
||||
if(ident){html+="<p class='wm-hero-sub'>"+ident+"</p>";}
|
||||
if(brand.homeIntro){html+="<p class='wm-hero-intro'>"+esc(brand.homeIntro)+"</p>";}
|
||||
html+="</header>";
|
||||
html+="<div class='wm-card'><h2 class='wm-card-title'>Status</h2>";
|
||||
if(boot.initialStatus){
|
||||
html+="<p class='wm-home-summary'>"+esc(boot.initialStatus)+"</p>";
|
||||
}
|
||||
if(summary){html+="<p class='wm-home-summary'>"+esc(summary)+"</p>";}
|
||||
if(timeoutSeconds>0){html+="<p class='wm-status' id='wm-portal-timeout'></p>";}
|
||||
html+="<p class='wm-lead'>Use <strong>WiFi</strong> to scan networks and connect. Open <strong>Info</strong> for addresses and firmware actions.</p>";
|
||||
html+="</div>";
|
||||
html+=renderExtraHomeCards(boot.extraHomeCards||[]);
|
||||
html+=deviceActionsHtml();
|
||||
setView(html);
|
||||
if(timeoutSeconds>0)startPortalTimeoutCountdown(timeoutSeconds);
|
||||
}
|
||||
|
||||
// --- HTTP ---
|
||||
@@ -159,24 +235,50 @@ function api(path, opt){
|
||||
|
||||
// --- WiFi scan view ---
|
||||
var _wmWifiScanPollTimer=null;
|
||||
var _wmWifiScanPollToken=0;
|
||||
|
||||
function getScanResultsBox(){
|
||||
return $('wm-scan-results');
|
||||
}
|
||||
|
||||
function setScanRefreshEnabled(enabled){
|
||||
var btn=$('wm-refresh-scan');
|
||||
if(btn)btn.disabled=(enabled===false);
|
||||
}
|
||||
|
||||
function stopWifiScanPolling(){
|
||||
_wmWifiScanPollToken++;
|
||||
if(_wmWifiScanPollTimer){
|
||||
clearInterval(_wmWifiScanPollTimer);
|
||||
clearTimeout(_wmWifiScanPollTimer);
|
||||
_wmWifiScanPollTimer=null;
|
||||
}
|
||||
setScanRefreshEnabled(true);
|
||||
}
|
||||
|
||||
function startWifiScanPolling(box){
|
||||
stopWifiScanPolling();
|
||||
_wmWifiScanPollTimer=setInterval(function(){
|
||||
setScanRefreshEnabled(false);
|
||||
var token=++_wmWifiScanPollToken;
|
||||
function poll(){
|
||||
api('/api/wifi/scan-status').then(function(res){
|
||||
if(token!==_wmWifiScanPollToken)return;
|
||||
var d={};
|
||||
try{d=JSON.parse(res.body);}catch(e){return;}
|
||||
box=getScanResultsBox()||box;
|
||||
if(box)box.innerHTML=renderScanList(d);
|
||||
if(!d.scanning)stopWifiScanPolling();
|
||||
if(!d.scanning){
|
||||
stopWifiScanPolling();
|
||||
return;
|
||||
}
|
||||
_wmWifiScanPollTimer=setTimeout(poll,800);
|
||||
}).catch(function(){
|
||||
if(token!==_wmWifiScanPollToken)return;
|
||||
stopWifiScanPolling();
|
||||
box=getScanResultsBox()||box;
|
||||
if(box)box.innerHTML=renderScanError('Portal became unreachable while scanning.');
|
||||
});
|
||||
},800);
|
||||
}
|
||||
poll();
|
||||
}
|
||||
|
||||
function renderScanList(data){
|
||||
@@ -197,23 +299,42 @@ function renderScanList(data){
|
||||
n=data.networks[i];
|
||||
pct=(n.quality||0)+'%';
|
||||
qi=Math.round((n.quality/100)*3)+1;if(qi<1)qi=1;if(qi>4)qi=4;
|
||||
enc=n.encrypted?'l':'';
|
||||
html+="<a class='wm-scan-row' href='#p' data-ssid='"+esc(n.ssid)+"'>";
|
||||
html+="<span class='wm-scan-ssid'>"+esc(n.ssid)+"</span>";
|
||||
html+="<span role='img' aria-label='"+pct+"' title='"+pct+"' class='q q-"+qi+" "+enc+"'></span>";
|
||||
html+="<span class='wm-scan-meta'>";
|
||||
if(n.encrypted){html+="<span class='wm-scan-lock q l' aria-hidden='true'></span>";}
|
||||
html+="<span role='img' aria-label='"+pct+"' title='"+pct+"' class='wm-scan-bars q q-"+qi+"'></span>";
|
||||
html+="<span class='wm-scan-signal'>"+pct+"</span>";
|
||||
html+="</span>";
|
||||
html+="</a>";
|
||||
}
|
||||
html+="</div>";
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderScanError(msg){
|
||||
return "<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>"+esc(msg||'Failed to reach portal while scanning.')+"</p></div>";
|
||||
}
|
||||
|
||||
function wifiRefresh(){
|
||||
var box=document.getElementById('wm-scan-results');
|
||||
var box=getScanResultsBox();
|
||||
if(!box)return;
|
||||
setScanRefreshEnabled(false);
|
||||
box.innerHTML="<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>Starting scan…</p></div>";
|
||||
api('/api/wifi/scan',{method:'POST'}).then(function(){
|
||||
api('/api/wifi/scan',{method:'POST'}).then(function(res){
|
||||
box=getScanResultsBox()||box;
|
||||
if(!res.ok){
|
||||
setScanRefreshEnabled(true);
|
||||
if(box)box.innerHTML=renderScanError('Failed to start WiFi scan.');
|
||||
showToast('Failed to start WiFi scan.',true);
|
||||
return;
|
||||
}
|
||||
startWifiScanPolling(box);
|
||||
}).catch(function(){
|
||||
stopWifiScanPolling();
|
||||
box=getScanResultsBox()||box;
|
||||
if(box)box.innerHTML=renderScanError('Failed to reach portal while starting scan.');
|
||||
showToast('Failed to reach portal while starting scan.',true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -273,12 +394,19 @@ function viewWifi(){
|
||||
html+="<div id='wm-wifi-msg'></div></div>";
|
||||
setView(html);
|
||||
bindWifiViewEvents();
|
||||
setScanRefreshEnabled(true);
|
||||
api('/api/wifi/scan-status').then(function(r2){
|
||||
try{var d=JSON.parse(r2.body);}catch(e){d={};}
|
||||
var box=document.getElementById('wm-scan-results');
|
||||
var box=getScanResultsBox();
|
||||
if(box)box.innerHTML=renderScanList(d);
|
||||
if(d&&d.scanning){startWifiScanPolling(box);}
|
||||
}).catch(function(){
|
||||
var box=getScanResultsBox();
|
||||
if(box)box.innerHTML=renderScanError('Failed to reach portal while loading scan status.');
|
||||
setScanRefreshEnabled(true);
|
||||
});
|
||||
}).catch(function(){
|
||||
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Failed to load WiFi options. Reconnect to the portal and try again.</p></div>");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -310,6 +438,10 @@ function pollWifiConnectStatus(){
|
||||
stopWifiConnectPolling();
|
||||
showToast(j.message||'WiFi connect failed',true);
|
||||
}
|
||||
}).catch(function(){
|
||||
stopWifiConnectPolling();
|
||||
var msg=$('wm-wifi-msg');
|
||||
if(msg)msg.innerHTML='Portal became unreachable while checking connection status.';
|
||||
});
|
||||
},700);
|
||||
}
|
||||
@@ -363,6 +495,15 @@ function viewInfo(){
|
||||
html+=section('Device',d.device||[]);
|
||||
html+=section('WiFi',d.wifi||[]);
|
||||
html+=section('About',d.about||[]);
|
||||
if(d.extraSections){
|
||||
var xi,zs,zrows,zi,it;
|
||||
for(xi=0;xi<d.extraSections.length;xi++){
|
||||
zs=d.extraSections[xi];
|
||||
zrows=[];
|
||||
if(zs.items){for(zi=0;zi<zs.items.length;zi++){it=zs.items[zi];zrows.push({label:it.label,value:esc(it.value)});}}
|
||||
html+=section(zs.title,zrows);
|
||||
}
|
||||
}
|
||||
var act=d.actions||{};
|
||||
html+="<div class='wm-card'><h2 class='wm-card-title'>Actions</h2><div class='wm-btn-group'>";
|
||||
if(act.showRestart)html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>Restart device</button>";
|
||||
@@ -426,10 +567,11 @@ window.portalParamSave=function(ev){
|
||||
};
|
||||
|
||||
function viewUpdate(){
|
||||
var f=boot.features||{};
|
||||
var pages=boot.pages||{};
|
||||
var pupd=pages.update||{};
|
||||
var html=navBar('update');
|
||||
html+="<div class='wm-page-head'><h1>Firmware update</h1><p class='wm-page-desc'>Flash a new firmware build (.bin). The device reboots when done.</p></div>";
|
||||
if(!f.showUpdate){
|
||||
if(!pupd.visible){
|
||||
html+="<div class='wm-card'><p class='wm-lead'>Firmware update is disabled for this configuration.</p></div>";
|
||||
setView(html);
|
||||
return;
|
||||
@@ -484,19 +626,29 @@ window.portalOtaSubmit=function(ev){
|
||||
function route(){
|
||||
var h=location.hash||'#/';
|
||||
if(h.indexOf('#/')!==0)h='#/';
|
||||
var f=boot.features||{};
|
||||
if(h==='#/info'&&f.showInfo===false){viewHome();return;}
|
||||
if(h==='#/update'&&f.showUpdate===false){viewHome();return;}
|
||||
if(h==='#/'||h==='#'){viewHome();return;}
|
||||
if(h==='#/wifi'){viewWifi();return;}
|
||||
if(h==='#/info'){viewInfo();return;}
|
||||
if(h==='#/setup'){viewSetup();return;}
|
||||
if(h==='#/update'){viewUpdate();return;}
|
||||
viewHome();
|
||||
var pages=boot.pages||{};
|
||||
var ps=pages.setup||{};
|
||||
var pi=pages.info||{};
|
||||
var pu=pages.update||{};
|
||||
if(h==='#/setup'&&ps.visible===false){viewHome();try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:'#/'}}));}catch(e){}return;}
|
||||
if(h==='#/info'&&pi.visible===false){viewHome();try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:'#/'}}));}catch(e){}return;}
|
||||
if(h==='#/update'&&pu.visible===false){viewHome();try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:'#/'}}));}catch(e){}return;}
|
||||
if(h==='#/'||h==='#'){viewHome();}
|
||||
else if(h==='#/wifi'){viewWifi();}
|
||||
else if(h==='#/info'){viewInfo();}
|
||||
else if(h==='#/setup'){viewSetup();}
|
||||
else if(h==='#/update'){viewUpdate();}
|
||||
else {viewHome();h='#/';}
|
||||
try{document.dispatchEvent(new CustomEvent('wm:view-changed',{detail:{route:h}}));}catch(e){}
|
||||
}
|
||||
|
||||
function bootPortal(){
|
||||
route();
|
||||
try{document.dispatchEvent(new CustomEvent('wm:ready',{detail:{boot:boot}}));}catch(e){}
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange',route);
|
||||
window.addEventListener('load',route);
|
||||
window.addEventListener('load',bootPortal);
|
||||
})();
|
||||
|
||||
)rawliteral";
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
* Single-page portal shell: only HTML document served for GET /.
|
||||
* Dynamic UI is driven by embedded JS + JSON APIs under /api/...
|
||||
*
|
||||
* Placeholder keys (%PAGE_TITLE%, %STYLES%, %BOOTSTRAP_JSON%, %PORTAL_APP_JS%) are filled per request in
|
||||
* WiFiManagerHandlers::handleRoot (shell contract comment in WiFiManagerHandlers.cpp); do not treat
|
||||
* placeholders as a customization API.
|
||||
* Placeholder keys (%PAGE_TITLE%, %STYLES%, %BOOTSTRAP_JSON%, %PORTAL_APP_JS%, %PORTAL_APPEND_JS%) are filled
|
||||
* per request in WiFiManagerHandlers::handleRoot; do not treat placeholders as a customization API.
|
||||
*/
|
||||
|
||||
#ifndef _WM_ROOT_SHELL_TEMPLATE_H_
|
||||
@@ -42,6 +41,9 @@ const char WM_ROOT_SHELL_TEMPLATE[] PROGMEM =
|
||||
"<script>"
|
||||
"%PORTAL_APP_JS%"
|
||||
"</script>"
|
||||
"<script>"
|
||||
"%PORTAL_APPEND_JS%"
|
||||
"</script>"
|
||||
"</body>"
|
||||
"</html>";
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ bool normalizeHostname(String& hostname) {
|
||||
* @param p Pointer to WiFiManagerParameter to add
|
||||
* @return true if added successfully, false on error
|
||||
*/
|
||||
bool WiFiManager::addParameter(WiFiManagerParameter *p) {
|
||||
bool WiFiManager::portalAddParameter(WiFiManagerParameter *p) {
|
||||
|
||||
// check param id is valid, unless null
|
||||
if(p->getID()){
|
||||
@@ -111,6 +111,15 @@ bool WiFiManager::addParameter(WiFiManagerParameter *p) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void WiFiManager::portalClearParameters() {
|
||||
if (_params != NULL) {
|
||||
free(_params);
|
||||
_params = NULL;
|
||||
}
|
||||
_paramsCount = 0;
|
||||
_max_params = WIFI_MANAGER_MAX_PARAMS;
|
||||
}
|
||||
|
||||
WiFiManagerParameter** WiFiManager::getParameters() {
|
||||
return _params;
|
||||
}
|
||||
@@ -1927,29 +1936,6 @@ void WiFiManager::setShowDnsFields(boolean alwaysShow){
|
||||
else _staShowDns = alwaysShow ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* toggle showing password in wifi password field
|
||||
* if not enabled, placeholder will be S_passph
|
||||
* @since $dev
|
||||
* @access public
|
||||
* @param boolean alwaysShow [false]
|
||||
*/
|
||||
void WiFiManager::setShowPassword(boolean show){
|
||||
_showPassword = show;
|
||||
}
|
||||
|
||||
/**
|
||||
* toggle captive portal
|
||||
* if enabled, then devices that use captive portal checks will be redirected to root
|
||||
* if not you will automatically have to navigate to ip [192.168.4.1]
|
||||
* @since $dev
|
||||
* @access public
|
||||
* @param boolean enabled [true]
|
||||
*/
|
||||
void WiFiManager::setCaptivePortalEnable(boolean enabled){
|
||||
_enableCaptivePortal = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* toggle wifi autoreconnect policy
|
||||
* if enabled, then wifi will autoreconnect automatically always
|
||||
@@ -2076,24 +2062,148 @@ void WiFiManager::setWiFiAPHidden(bool hidden){
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* toggle showing erase wifi config button on info page
|
||||
* @param boolean enabled
|
||||
*/
|
||||
void WiFiManager::setShowInfoErase(boolean enabled){
|
||||
_showInfoErase = enabled;
|
||||
void WiFiManager::portalSetBrandTitle(const String& title) {
|
||||
_portalBrand.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* toggle showing update upload web ota button on info page
|
||||
* @param boolean enabled
|
||||
*/
|
||||
void WiFiManager::setShowInfoUpdate(boolean enabled){
|
||||
_showInfoUpdate = enabled;
|
||||
void WiFiManager::portalSetContextIdentityText(const String& identityText) {
|
||||
_portalBrand.identityTextOverride = identityText;
|
||||
}
|
||||
|
||||
void WiFiManager::setShowInfo(boolean enabled){
|
||||
_showInfo = enabled;
|
||||
void WiFiManager::portalSetBrandHomeIntro(const String& text) {
|
||||
_portalBrand.homeIntro = text;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBrandLogoSvg(const String& svgMarkup) {
|
||||
_portalBrand.logoSvg = svgMarkup;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetPageInfoVisible(bool visible) {
|
||||
_portalPages.infoVisible = visible;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetPageUpdateVisible(bool visible) {
|
||||
_portalPages.updateVisible = visible;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetPageSetupVisible(bool visible) {
|
||||
_portalPages.setupVisible = visible;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetActionEraseVisible(bool visible) {
|
||||
_portalActions.eraseVisible = visible;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetActionRestartVisible(bool visible) {
|
||||
_portalActions.restartVisible = visible;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetActionExitVisible(bool visible) {
|
||||
_portalActions.exitVisible = visible;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetActionCloseCaptiveVisible(bool visible) {
|
||||
_portalActions.closeCaptiveVisible = visible;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetActionBackVisible(bool visible) {
|
||||
_portalActions.backVisible = visible;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetLayoutParamsLocation(PortalParamsLocation location) {
|
||||
_portalLayout.paramsOnWifiPage = (location == PortalParamsLocation::WiFiPage);
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBehaviorCaptivePortalEnabled(bool enabled) {
|
||||
_enableCaptivePortal = enabled;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBehaviorConnectOnSave(bool enabled) {
|
||||
setSaveConnect(enabled);
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBehaviorExitAllowed(bool allowed) {
|
||||
_allowExit = allowed;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBehaviorConnectTimeoutSeconds(unsigned long seconds) {
|
||||
setConnectTimeout(seconds);
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBehaviorPortalTimeoutSeconds(unsigned long seconds) {
|
||||
setConfigPortalTimeout(seconds);
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBehaviorAutoReconnect(bool enabled) {
|
||||
setWiFiAutoReconnect(enabled);
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBehaviorApClientCheck(bool enabled) {
|
||||
setAPClientCheck(enabled);
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBehaviorWebClientCheck(bool enabled) {
|
||||
setWebPortalClientCheck(enabled);
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetFieldPasswordPlaceholderMode(PortalPasswordPlaceholderMode mode) {
|
||||
_portalPasswordPlaceholderMode = mode;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetFieldStaticIpVisibility(PortalFieldVisibility visibility) {
|
||||
switch (visibility) {
|
||||
case PortalFieldVisibility::Always:
|
||||
setShowStaticFields(true);
|
||||
break;
|
||||
case PortalFieldVisibility::Auto:
|
||||
setShowStaticFields(false);
|
||||
break;
|
||||
case PortalFieldVisibility::Hidden:
|
||||
_staShowStaticFields = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetFieldStaticDnsVisibility(PortalFieldVisibility visibility) {
|
||||
switch (visibility) {
|
||||
case PortalFieldVisibility::Always:
|
||||
setShowDnsFields(true);
|
||||
break;
|
||||
case PortalFieldVisibility::Auto:
|
||||
setShowDnsFields(false);
|
||||
break;
|
||||
case PortalFieldVisibility::Hidden:
|
||||
_staShowDns = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiManager::portalAddInfoSection(const PortalInfoSection& section) {
|
||||
_portalStructured.infoSections.push_back(section);
|
||||
}
|
||||
|
||||
void WiFiManager::portalClearInfoSections() {
|
||||
_portalStructured.infoSections.clear();
|
||||
}
|
||||
|
||||
void WiFiManager::portalAddHomeCard(const PortalHomeCard& card) {
|
||||
_portalStructured.homeCards.push_back(card);
|
||||
}
|
||||
|
||||
void WiFiManager::portalClearHomeCards() {
|
||||
_portalStructured.homeCards.clear();
|
||||
}
|
||||
|
||||
void WiFiManager::portalAppendCss(const String& css) {
|
||||
_portalAssets.appendedCss += css;
|
||||
}
|
||||
|
||||
void WiFiManager::portalOverrideCss(const String& css) {
|
||||
_portalAssets.overriddenCss = css;
|
||||
}
|
||||
|
||||
void WiFiManager::portalAppendJs(const String& js) {
|
||||
_portalAssets.appendedJs += js;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2174,18 +2284,6 @@ DNSServer* WiFiManager::getDNSServer() {
|
||||
return _serverManager ? _serverManager->getDNSServer() : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* [setTitle description]
|
||||
* @param String title, set app title
|
||||
*/
|
||||
void WiFiManager::setTitle(String title){
|
||||
_title = title;
|
||||
}
|
||||
|
||||
void WiFiManager::setParametersEmbeddedInWifi(boolean embedded) {
|
||||
_paramsInWifi = embedded;
|
||||
}
|
||||
|
||||
// GETTERS
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "templates/PortalAppJS.h"
|
||||
#include <TemplateEngine.h>
|
||||
#include <cstring>
|
||||
|
||||
#if defined(ESP8266) || defined(ESP32)
|
||||
|
||||
#ifndef WM_PAGE_RESERVE_BYTES
|
||||
@@ -54,12 +53,42 @@ size_t dynamicStringLengthGetter(const char* data, void* /*userData*/) {
|
||||
return data ? strlen(data) : 0;
|
||||
}
|
||||
|
||||
void configureDynamicStringDescriptor(DynamicTemplateDescriptor& descriptor, String& value) {
|
||||
void configureDynamicStringDescriptor(DynamicDataDescriptor& descriptor, String& value) {
|
||||
descriptor.getter = &dynamicStringGetter;
|
||||
descriptor.getLength = &dynamicStringLengthGetter;
|
||||
descriptor.userData = &value;
|
||||
}
|
||||
|
||||
String readProgmemString(const char* p) {
|
||||
return String(FPSTR(p));
|
||||
}
|
||||
|
||||
String stripStyleWrapper(const String& styleBlock) {
|
||||
String css = styleBlock;
|
||||
const String openTag = F("<style>");
|
||||
const String closeTag = F("</style>");
|
||||
|
||||
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 <typename BundleT>
|
||||
AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request,
|
||||
const std::shared_ptr<BundleT>& 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("<style>");
|
||||
out += css;
|
||||
out += F("</style>");
|
||||
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<const WiFiManager::WiFiScanNetwork*>& 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<PortalShellRenderBundle>();
|
||||
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("}}");
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(¶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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user