Portal: request-scoped shell bundle, SPA connect poll fix, test tidy

- Use per-request PortalShellRenderBundle and one root shell template; drop
  server-side template registry and simplify WiFiManagerServer.
- PortalAppJS: centralize chrome/form binding; fix WiFi connect polling by
  stopping previous timers on view change and before new polls.
- processPortalConnect honors _cleanConnect like connectWifi.
- Remove brittle full-JSON API tests; trim test harness surface.
- README: small doc touch-up for the shell/portal flow.
This commit is contained in:
2026-05-02 20:19:44 +10:00
parent bb1629af90
commit 66e6088bf1
11 changed files with 127 additions and 168 deletions
+4 -3
View File
@@ -31,10 +31,11 @@ This fork currently includes the following architectural improvements:
- **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.
- **SPA-native feedback UX** using in-DOM dialog/toast behavior rather than page-based action flows.
- A **single shell render contract** with default shell placeholders (`%PAGE_TITLE%`, `%STYLES%`) plus embedded runtime payloads (`%BOOTSTRAP_JSON%`, `%PORTAL_APP_JS%`).
- **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`, `setParamsPage`, bootstrap-facing flags, etc.), not by exposing placeholder-registry mutation to consumers.
- A clearer separation between:
- shell rendering
- API responses
- shell rendering (handlers + SPA bootstrap)
- JSON API responses
- captive portal behavior
- OTA handling
- Updated tests focused on the **shell contract**, **bootstrap payloads**, and **API JSON shapes** rather than removed legacy portal pages.
@@ -16,6 +16,14 @@
#include <memory>
#include "WiFiManager.h"
#ifndef WM_TEMPLATE_REGISTRY_CAPACITY
#define WM_TEMPLATE_REGISTRY_CAPACITY 16
#endif
// Portal UI customization is driven by WiFiManager state and helpers such as setTitle(), setShowInfo(),
// setParamsPage(), setShowInfoUpdate(), etc., surfaced through bootstrap/API JSON — not by mutating template
// placeholder registries.
// -----------------------------------------------------------------------------------------------
// FORM FIELD NAMES (for IP configuration forms)
@@ -79,6 +87,7 @@ class WiFiManagerHandlers {
String buildApiWifiConnectStatusJson();
/** Fixed JSON bodies for POST action endpoints (single source for handlers + tests). */
static String jsonApiWifiScanAccepted();
static String jsonApiDeviceRestartScheduled();
static String jsonApiParamsSaveOk();
static String jsonApiPortalCloseOk();
@@ -37,12 +37,6 @@
#include <functional>
#include <memory>
#include <TemplateEngine.h>
#ifndef WM_TEMPLATE_REGISTRY_CAPACITY
#define WM_TEMPLATE_REGISTRY_CAPACITY 16
#endif
class WiFiManager;
class WiFiManagerHandlers;
@@ -74,21 +68,9 @@ const char R_updatedone[] PROGMEM = "/u";
class WiFiManagerServer {
public:
static WiFiManagerServer* instance();
WiFiManagerServer(WiFiManager* wm);
~WiFiManagerServer();
void setupTemplateEngine();
PlaceholderRegistry* getPlaceholderRegistry() { return _tplRegistry.get(); }
static const char* tplGetPageTitle();
void registerDefaultStyles(PlaceholderRegistry& reg);
void registerDefaultPageTitle(PlaceholderRegistry& reg);
/** Shell placeholders only: %STYLES%, %PAGE_TITLE%. */
void registerDefaultPlaceholders(PlaceholderRegistry& reg);
void createServer(uint16_t port);
void registerRoutes();
void setupDNSD();
@@ -103,14 +85,10 @@ class WiFiManagerServer {
std::unique_ptr<AsyncWebServer> server;
std::unique_ptr<DNSServer> dnsServer;
std::unique_ptr<PlaceholderRegistry> _tplRegistry;
#ifdef WM_DFTE_LOGGING
std::unique_ptr<WiFiManagerDfteLogger> _dfteLogger;
bool _wmOwnsDfteLogSink = false;
#endif
static WiFiManagerServer* s_instance;
};
#endif // defined(ESP8266) || defined(ESP32)
+58 -15
View File
@@ -18,9 +18,11 @@ const char PORTAL_APP_JS[] PROGMEM = R"rawliteral(
var boot={};
try{var el=document.getElementById('wm-bootstrap');if(el&&el.textContent)boot=JSON.parse(el.textContent);}catch(e){boot={};}
// --- Core / DOM ---
function $(id){return document.getElementById(id);}
function esc(t){if(!t)return'';return String(t).replace(/[&<>"']/g,function(c){return({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]);});}
// --- Toast / dialog ---
var _wmToastTimer=null;
function showToast(msg,isErr){
var el=$('wm-toast');
@@ -50,13 +52,37 @@ function showConfirm(msg,onOk){
ok.onclick=function(){hide();if(onOk)onOk();};
}
function bindPortalChromeEvents(){
var back=$('wm-nav-back');
if(back){
back.addEventListener('click', function(ev){ ev.preventDefault(); history.back(); });
}
var r=$('wm-restart-btn'), e=$('wm-exit-btn'), c=$('wm-close-captive-btn'), er=$('wm-erase-btn');
if(r && window.portalRestart) r.addEventListener('click', window.portalRestart);
if(e && window.portalExit) e.addEventListener('click', window.portalExit);
if(c && window.portalCloseCaptive) c.addEventListener('click', window.portalCloseCaptive);
if(er && window.portalErase) er.addEventListener('click', window.portalErase);
}
function bindFormSubmitHandlers(){
var wf=$('wm-wifi-form'), pf=$('wm-param-form'), of=$('wm-ota-form');
if(wf && window.portalWifiSave) wf.addEventListener('submit', window.portalWifiSave);
if(pf && window.portalParamSave) pf.addEventListener('submit', window.portalParamSave);
if(of && window.portalOtaSubmit) of.addEventListener('submit', window.portalOtaSubmit);
}
// --- View shell ---
function setView(html){
var a=$('app');
if(!a)return;
stopWifiScanPolling();
stopWifiConnectPolling();
a.innerHTML="<div class='wrap'>"+html+"</div>";
bindPortalChromeEvents();
bindFormSubmitHandlers();
}
// --- Field render (WiFi / params meta) ---
function renderMetaField(f){
if(!f)return'';
if(f.html)return f.html;
@@ -89,7 +115,7 @@ function navBar(){
if(!f.paramsInWifi){h+=" &middot; <a href='#/setup'>Setup</a>";}
if(f.showInfo!==false){h+=" &middot; <a href='#/info'>Info</a>";}
if(f.showUpdate){h+=" &middot; <a href='#/update'>Update</a>";}
if(boot.showBack){h+=" &middot; <a href='#' onclick='history.back();return false;'>Back</a>";}
if(boot.showBack){h+=" &middot; <a href='#' id='wm-nav-back'>Back</a>";}
h+="</p><hr/>";
return h;
}
@@ -97,9 +123,9 @@ function navBar(){
function deviceActionsHtml(){
var f=boot.features||{};
var h='', ok=false;
if(f.showRestart){h+="<p><button type='button' onclick='portalRestart()'>Restart</button></p>";ok=true;}
if(f.showExitPortal){h+="<p><button type='button' onclick='portalExit()'>Exit portal</button></p>";ok=true;}
if(f.showCloseCaptive){h+="<p><button type='button' onclick='portalCloseCaptive()'>Stop captive portal detection</button></p>";ok=true;}
if(f.showRestart){h+="<p><button type='button' id='wm-restart-btn'>Restart</button></p>";ok=true;}
if(f.showExitPortal){h+="<p><button type='button' id='wm-exit-btn'>Exit portal</button></p>";ok=true;}
if(f.showCloseCaptive){h+="<p><button type='button' id='wm-close-captive-btn'>Stop captive portal detection</button></p>";ok=true;}
if(!ok)return'';
return "<div class='device-actions'><h3>Device actions</h3>"+h+"</div>";
}
@@ -115,12 +141,14 @@ function viewHome(){
setView(html);
}
// --- HTTP ---
function api(path, opt){
opt=opt||{};
return fetch(path,{method:opt.method||'GET',headers:opt.headers,body:opt.body,credentials:'same-origin'})
.then(function(r){return r.text().then(function(t){return {ok:r.ok,status:r.status,body:t};});});
}
// --- WiFi scan view ---
var _wmWifiScanPollTimer=null;
function stopWifiScanPolling(){
@@ -206,7 +234,7 @@ function viewWifi(){
try{var m=JSON.parse(res.body);}catch(e){m={};}
var html=navBar()+"<h2>WiFi</h2>";
html+="<div id='wm-scan-results'></div>";
html+="<form id='wm-wifi-form' onsubmit='return portalWifiSave(event)'>";
html+="<form id='wm-wifi-form'>";
html+=renderFieldList(m.wifiFields||[]);
var wf=m.wifiFields||[];
var hasPass=false;
@@ -231,26 +259,38 @@ function viewWifi(){
});
}
// --- WiFi connect polling ---
var _wmWifiConnectPollTimer=null;
function stopWifiConnectPolling(){
if(_wmWifiConnectPollTimer){
clearInterval(_wmWifiConnectPollTimer);
_wmWifiConnectPollTimer=null;
}
}
function pollWifiConnectStatus(){
var msg=$('wm-wifi-msg');
var iv=setInterval(function(){
stopWifiConnectPolling();
_wmWifiConnectPollTimer=setInterval(function(){
api('/api/wifi/connect-status').then(function(res){
var j={};
var msg=$('wm-wifi-msg');
try{j=JSON.parse(res.body);}catch(e){}
if(msg&&j.message){
msg.innerHTML=esc(j.message);
}
if(j.state==='success'){
clearInterval(iv);
stopWifiConnectPolling();
showToast(j.message||'WiFi connected',false);
location.hash='#/';
}else if(j.state==='failed'){
clearInterval(iv);
stopWifiConnectPolling();
showToast(j.message||'WiFi connect failed',true);
}
});
},700);
}
window.portalWifiSave=function(ev){
ev.preventDefault();
var fd=new FormData(document.getElementById('wm-wifi-form'));
@@ -269,6 +309,7 @@ window.portalWifiSave=function(ev){
return false;
};
// --- Info view ---
function viewInfo(){
setView(navBar()+"<p>Loading info...</p>");
api('/api/info').then(function(res){
@@ -300,11 +341,11 @@ function viewInfo(){
html+=section('About',d.about||[]);
var act=d.actions||{};
html+="<h3>Actions</h3><hr/>";
if(act.showRestart)html+="<p><button type='button' onclick='portalRestart()'>Restart</button></p>";
if(act.showExitPortal)html+="<p><button type='button' onclick='portalExit()'>Exit portal</button></p>";
if(act.showCloseCaptive)html+="<p><button type='button' onclick='portalCloseCaptive()'>Stop captive portal detection</button></p>";
if(act.showRestart)html+="<p><button type='button' id='wm-restart-btn'>Restart</button></p>";
if(act.showExitPortal)html+="<p><button type='button' id='wm-exit-btn'>Exit portal</button></p>";
if(act.showCloseCaptive)html+="<p><button type='button' id='wm-close-captive-btn'>Stop captive portal detection</button></p>";
if(act.showUpdate)html+="<p><a href='#/update'>Firmware update</a></p>";
if(act.showErase)html+="<p><button type='button' class='D' onclick='portalErase()'>Erase WiFi config</button></p>";
if(act.showErase)html+="<p><button type='button' class='D' id='wm-erase-btn'>Erase WiFi config</button></p>";
setView(html);
});
}
@@ -332,12 +373,13 @@ window.portalErase=function(){
});
};
// --- Params / OTA ---
function viewSetup(){
setView(navBar()+"<p>Loading...</p>");
api('/api/params').then(function(res){
try{var d=JSON.parse(res.body);}catch(e){d={};}
var html=navBar()+"<h2>Parameters</h2>";
html+="<form id='wm-param-form' onsubmit='return portalParamSave(event)'>";
html+="<form id='wm-param-form'>";
html+=renderFieldList(d.params||[]);
html+="<button type='submit'>Save</button></form>";
html+="<div id='wm-param-msg'></div>";
@@ -361,7 +403,7 @@ function viewUpdate(){
var f=boot.features||{};
var html=navBar()+"<h2>Firmware update</h2>";
if(!f.showUpdate){html+="<p>Firmware update is disabled.</p>";setView(html);return;}
html+="<form id='wm-ota-form' onsubmit='return portalOtaSubmit(event)'>";
html+="<form id='wm-ota-form'>";
html+="<input type='file' id='wm-ota-file' name='update'/>";
html+="<button type='submit'>Upload</button></form>";
html+="<p><small>Upload a .bin firmware. The device restarts after a successful update.</small></p>";
@@ -405,6 +447,7 @@ window.portalOtaSubmit=function(ev){
return false;
};
// --- Routing ---
function route(){
var h=location.hash||'#/';
if(h.indexOf('#/')!==0)h='#/';
@@ -6,6 +6,10 @@
*
* 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.
*/
#ifndef _WM_ROOT_SHELL_TEMPLATE_H_
+3
View File
@@ -727,6 +727,9 @@ void WiFiManager::processPortalConnect() {
return;
case wm_cp_connect_state_t::starting: {
setSTAConfig();
if (_cleanConnect) {
WiFi_Disconnect(); // match connectWifi(): force a clean STA begin for portal saves too
}
if (!_connectonsave) {
if (!wifiConnectNew(_cpConnectSsid, _cpConnectPass, false)) {
failPortalConnect(WL_CONNECT_FAILED, F("WiFi begin failed"));
+34 -30
View File
@@ -8,7 +8,6 @@
*/
#include "WiFiManagerHandlers.h"
#include "WiFiManagerServer.h"
#include "templates/CSS.h"
#include "templates/RootShell.h"
#include "templates/PortalAppJS.h"
@@ -40,8 +39,6 @@ static void jsonAppendEscaped(String& out, const String& s) {
namespace {
const char kEmptyTemplateChunk[] PROGMEM = "";
inline void reservePage(String& page, size_t extraBytes = WM_PAGE_RESERVE_BYTES) {
if (extraBytes == 0) return;
const size_t targetLen = page.length() + extraBytes;
@@ -76,16 +73,13 @@ AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request,
);
}
void registerSharedShellPlaceholders(WiFiManagerServer* server, PlaceholderRegistry& registry) {
if (server) {
server->registerDefaultStyles(registry);
return;
}
// 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
registry.registerProgmemData("%STYLES%", CSS_STYLE);
}
struct ShellRenderBundle {
struct PortalShellRenderBundle {
String bootstrapJson;
String pageTitleStatic;
PlaceholderRegistry registry;
@@ -93,10 +87,23 @@ struct ShellRenderBundle {
DynamicTemplateDescriptor bootstrapDescriptor;
DynamicTemplateDescriptor pageTitleDescriptor;
ShellRenderBundle()
PortalShellRenderBundle()
: registry(WM_TEMPLATE_REGISTRY_CAPACITY), bootstrapDescriptor{}, pageTitleDescriptor{} {}
};
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) {}
@@ -547,22 +554,15 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) {
if (captivePortal(request)) return;
handleRequest(request);
auto bundle = std::make_shared<ShellRenderBundle>();
bundle->bootstrapJson = buildPortalBootstrapJson();
if (_wm->_serverManager) {
_wm->_serverManager->registerDefaultStyles(bundle->registry);
_wm->_serverManager->registerDefaultPageTitle(bundle->registry);
} else {
registerSharedShellPlaceholders(nullptr, bundle->registry);
bundle->pageTitleStatic = _wm->_title;
configureDynamicStringDescriptor(bundle->pageTitleDescriptor, bundle->pageTitleStatic);
bundle->registry.registerDynamicTemplate("%PAGE_TITLE%", &bundle->pageTitleDescriptor);
}
configureDynamicStringDescriptor(bundle->bootstrapDescriptor, bundle->bootstrapJson);
bundle->registry.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bundle->bootstrapDescriptor);
bundle->registry.registerProgmemData("%PORTAL_APP_JS%", PORTAL_APP_JS);
// Root shell render model:
// - WiFiManagerHandlers owns request-time shell assembly; WiFiManagerServer owns HTTP lifecycle only.
// - Build one request-scoped PortalShellRenderBundle.
// - 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());
request->send(beginTemplateResponse(request, bundle, WM_ROOT_SHELL_TEMPLATE));
}
@@ -1011,7 +1011,7 @@ void WiFiManagerHandlers::handleApiWifiScan(AsyncWebServerRequest *request) {
_wm->requestAsyncScan(true);
AsyncWebServerResponse *response =
request->beginResponse(202, "application/json", "{\"accepted\":true,\"state\":\"queued\"}");
request->beginResponse(202, "application/json", jsonApiWifiScanAccepted());
response->addHeader(F("Cache-Control"), F("no-cache"));
request->send(response);
}
@@ -1181,6 +1181,10 @@ void WiFiManagerHandlers::handleApiStatus(AsyncWebServerRequest *request) {
sendApiJson(request, 200, buildApiStatusJson());
}
String WiFiManagerHandlers::jsonApiWifiScanAccepted() {
return F("{\"accepted\":true,\"state\":\"queued\"}");
}
String WiFiManagerHandlers::jsonApiDeviceRestartScheduled() {
return F("{\"ok\":true,\"message\":\"Restart scheduled\"}");
}
+15 -56
View File
@@ -4,7 +4,8 @@
* @author alexhopeoconnor
* @license MIT
*
* Server lifecycle: single shell + JSON APIs + OTA + captive portal.
* HTTP server lifecycle and route registration only. HTML shell rendering is request-scoped in
* WiFiManagerHandlers (WM_ROOT_SHELL_TEMPLATE + per-request placeholder registry).
* Route inventory matches WiFiManagerServer.h (GET /, /api/..., POST /u, onNotFound).
*/
@@ -15,61 +16,14 @@
#include "WiFiManagerDfteLogger.h"
#include <DeviceFrameworkTemplateEngineDebug.h>
#endif
#include "templates/CSS.h"
#if defined(ESP8266) || defined(ESP32)
WiFiManagerServer* WiFiManagerServer::s_instance = nullptr;
WiFiManagerServer* WiFiManagerServer::instance() {
return s_instance;
}
WiFiManagerServer::WiFiManagerServer(WiFiManager* wm)
: _wm(wm), _handlers(std::make_unique<WiFiManagerHandlers>(wm)) {
s_instance = this;
}
: _wm(wm), _handlers(std::make_unique<WiFiManagerHandlers>(wm)) {}
WiFiManagerServer::~WiFiManagerServer() = default;
const char* WiFiManagerServer::tplGetPageTitle() {
auto srv = WiFiManagerServer::instance();
if (!srv || !srv->_wm) return "";
return srv->_wm->_title.c_str();
}
void WiFiManagerServer::registerDefaultStyles(PlaceholderRegistry& reg) {
reg.registerProgmemData("%STYLES%", CSS_STYLE);
}
void WiFiManagerServer::registerDefaultPageTitle(PlaceholderRegistry& reg) {
reg.registerRamData("%PAGE_TITLE%", &WiFiManagerServer::tplGetPageTitle);
}
void WiFiManagerServer::registerDefaultPlaceholders(PlaceholderRegistry& reg) {
registerDefaultStyles(reg);
registerDefaultPageTitle(reg);
}
void WiFiManagerServer::setupTemplateEngine() {
if (!_tplRegistry) {
_tplRegistry = std::unique_ptr<PlaceholderRegistry>(new PlaceholderRegistry(WM_TEMPLATE_REGISTRY_CAPACITY));
} else {
_tplRegistry->clear();
}
registerDefaultPlaceholders(*_tplRegistry);
#ifdef WM_DFTE_LOGGING
if (!deviceFrameworkTemplateEngineIsLoggingEnabled()) {
if (!_dfteLogger) {
_dfteLogger = std::make_unique<WiFiManagerDfteLogger>(_wm);
}
deviceFrameworkTemplateEngineEnableLogging(_dfteLogger.get(), static_cast<const void*>(this));
_wmOwnsDfteLogSink = true;
}
#endif
}
void WiFiManagerServer::createServer(uint16_t port) {
if (server) {
#ifndef WM_NO_LOG
@@ -89,7 +43,18 @@ void WiFiManagerServer::createServer(uint16_t port) {
}
server.reset(new AsyncWebServer(port));
setupTemplateEngine();
#ifdef WM_DFTE_LOGGING
// DFTE logging is still owned by server lifecycle; it is intentionally independent of
// request-scoped shell rendering / placeholder registration.
if (!deviceFrameworkTemplateEngineIsLoggingEnabled()) {
if (!_dfteLogger) {
_dfteLogger = std::make_unique<WiFiManagerDfteLogger>(_wm);
}
deviceFrameworkTemplateEngineEnableLogging(_dfteLogger.get(), static_cast<const void*>(this));
_wmOwnsDfteLogSink = true;
}
#endif
}
void WiFiManagerServer::registerRoutes() {
@@ -100,10 +65,6 @@ void WiFiManagerServer::registerRoutes() {
return;
}
if (!_tplRegistry) {
setupTemplateEngine();
}
if (_wm->_webservercallback != NULL) {
#ifndef WM_NO_LOG
_wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("[CB] _webservercallback calling"));
@@ -212,8 +173,6 @@ void WiFiManagerServer::shutdownServer() {
dnsServer->stop();
dnsServer.reset();
}
if (s_instance == this) s_instance = nullptr;
}
#endif
-2
View File
@@ -15,8 +15,6 @@ TestCase tests[] = {
TEST_ENTRY(test_api_info_json_shape),
TEST_ENTRY(test_api_params_json_shape),
TEST_ENTRY(test_api_status_json_shape),
TEST_ENTRY(test_api_action_response_json_literals),
TEST_ENTRY(test_api_ota_done_success_json_literal),
// Configuration tests
TEST_ENTRY(test_configuration_setters),
-2
View File
@@ -131,8 +131,6 @@ void test_api_wifi_meta_json_shape();
void test_api_info_json_shape();
void test_api_params_json_shape();
void test_api_status_json_shape();
void test_api_action_response_json_literals();
void test_api_ota_done_success_json_literal();
// State transition tests
void test_portal_to_connected_transition();
@@ -81,41 +81,3 @@ void test_api_status_json_shape() {
Serial.println("[TEST] API status JSON shape test completed successfully");
}
void test_api_action_response_json_literals() {
Serial.println("[TEST] Testing POST /api action JSON payloads...");
TEST_ASSERT_EQUAL_STRING(
"{\"ok\":true,\"message\":\"Restart scheduled\"}",
WiFiManagerHandlers::jsonApiDeviceRestartScheduled().c_str());
TEST_ASSERT_EQUAL_STRING(
"{\"ok\":true,\"message\":\"Setup saved\"}",
WiFiManagerHandlers::jsonApiParamsSaveOk().c_str());
TEST_ASSERT_EQUAL_STRING(
"{\"ok\":true,\"message\":\"Captive portal detection disabled\"}",
WiFiManagerHandlers::jsonApiPortalCloseOk().c_str());
TEST_ASSERT_EQUAL_STRING(
"{\"ok\":true,\"message\":\"Exiting portal\"}",
WiFiManagerHandlers::jsonApiPortalExitOk().c_str());
TEST_ASSERT_EQUAL_STRING(
"{\"ok\":false,\"message\":\"Exit not allowed\"}",
WiFiManagerHandlers::jsonApiPortalExitForbidden().c_str());
TEST_ASSERT_EQUAL_STRING(
"{\"ok\":true,\"message\":\"WiFi configuration erased. Device will restart shortly.\"}",
WiFiManagerHandlers::jsonApiEraseResponse(true).c_str());
TEST_ASSERT_EQUAL_STRING(
"{\"ok\":false,\"message\":\"Erase failed\"}",
WiFiManagerHandlers::jsonApiEraseResponse(false).c_str());
Serial.println("[TEST] Action response JSON literals test completed successfully");
}
void test_api_ota_done_success_json_literal() {
Serial.println("[TEST] Testing POST /u completion success JSON...");
TEST_ASSERT_EQUAL_STRING(
"{\"ok\":true,\"message\":\"Firmware updated. Restarting...\"}",
WiFiManagerHandlers::jsonApiOtaUpdateSuccess().c_str());
Serial.println("[TEST] OTA success JSON literal test completed successfully");
}