Files
HaloKeymind/test/test_webconfig_ui.py
T
mikecarper 267773df7d Merge PR #13 with gzip-only WebConfig assets
Keep the graceful final-ACK TCP close and optional FEM control guards from
pixitha's browser compatibility fixes. Replace Brotli negotiation and duplicate
page assets with one gzip representation compressed by Google Zopfli 0.4.3
using 1,000 iterations. Compress the bootstrap, captive success, compact OTA,
and static WiFi setup pages with the same validated build cache.

Honor gzip quality values and wildcard precedence. Validate compressed OTA
page evidence in the firmware capability gate, and add native and Python
regressions plus CI coverage. Remove the unused Brotli build dependency.

Validation: 54 native tests, 56 Python/browser checks, full V4.3 build and
verified application flash. Actual V4 WiFi responses matched the generated
gzip assets; 30 repeat downloads completed without reboot. Chromium loaded
the setup and settings pages without script or transfer errors. Physical
Android testing is pending because the Pi detects no phone over USB or mDNS.
2026-09-30 18:55:17 -07:00

271 lines
14 KiB
Python

#!/usr/bin/env python3
import gzip
from pathlib import Path
import re
import unittest
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "webui" / "index.html"
HEADER = ROOT / "src" / "helpers" / "esp32" / "WebConfigHtml.h"
class WebConfigUiTest(unittest.TestCase):
def test_wifi_password_fields_have_local_show_controls(self):
page = SOURCE.read_text(encoding="utf-8")
self.assertEqual(page.count('onclick="toggleWiFiPassword(this)"'), 2)
self.assertEqual(page.count('class="pwrow"'), 2)
self.assertIn('type="password" data-k="wifi.pwd" id="wz-wifi-pwd"', page)
self.assertIn('type="password" data-k="wifi.pwd" id="app-wifi-pwd"', page)
self.assertIn('input.type=show?"text":"password"', page)
self.assertIn('btn.textContent=show?"Hide":"Show"', page)
self.assertIn('btn.setAttribute("aria-pressed",show?"true":"false")', page)
def test_setup_wizard_can_skip_mqtt_and_usa_radio(self):
page = SOURCE.read_text(encoding="utf-8")
self.assertIn('id="wz-radio-skip" onclick="wzSkipRadio()"', page)
self.assertIn('onclick="wzSkipMqtt()">Skip MQTT</button>', page)
self.assertIn("st.radioOptional=!!s.radio_optional", page)
radio_skip = page[page.index("function wzSkipRadio()"):
page.index("function wzUseMqtt()")]
self.assertIn("if(!st.radioOptional)return", radio_skip)
self.assertIn("delete st.dirty.radio", radio_skip)
self.assertIn("delete st.dirty.tx", radio_skip)
mqtt_skip = page[page.index("function wzSkipMqtt()"):
page.index("function popOut()")]
self.assertIn("st.skipMqtt=true", mqtt_skip)
self.assertIn("/^mqtt(?:\\.|[1-9]\\.)/", mqtt_skip)
self.assertIn("syncPacketFilters()", mqtt_skip)
save = page[page.index("function wizardSave()"):
page.index("/* ---------- reboot ---------- */")]
self.assertIn('if(!st.skipRadio&&$("#wz-rp").value==="")', save)
self.assertIn("if(st.hasMqtt&&!st.skipMqtt&&", save)
def test_setup_wizard_opens_existing_wifi_scan_once(self):
page = SOURCE.read_text(encoding="utf-8")
self.assertIn("wizardScanOpened:false", page)
self.assertIn('onclick="openScan(\'wz-ssid\')">Scan</button>', page)
self.assertIn('onclick="startScan(true)">Rescan</button>', page)
open_scan = page[page.index("function openScan(target)"):
page.index("function closeScan()")]
self.assertIn('if(target==="wz-ssid")st.wizardScanOpened=true', open_scan)
auto_open = page[page.index("function openWizardScanOnce()"):
page.index("function wzGo(n)")]
self.assertIn("if(st.wizardScanOpened)return", auto_open)
self.assertIn('openScan("wz-ssid")', auto_open)
self.assertNotIn("startScan(true)", auto_open)
show_wizard = page[page.index("function showWizard()"):
page.index("function wzGo(n)")]
self.assertLess(show_wizard.index("loadConfig(capture)"),
show_wizard.index("openWizardScanOnce()"))
self.assertIn("wzRadioFill();openWizardScanOnce()", show_wizard)
# Scan completion may replace only the result list. Credentials change
# only in the existing explicit network-click handler below it.
poll_scan = page[page.index("function pollScan(url,epoch)"):
page.index("function clearInheritedWiFiPassword")]
self.assertNotIn(".value=", poll_scan)
self.assertNotIn("dispatchEvent", poll_scan)
def test_scan_epoch_invalidates_old_timers_and_responses(self):
page = SOURCE.read_text(encoding="utf-8")
self.assertIn("scanEpoch:0", page)
close_scan = page[page.index("function closeScan()"):
page.index("function startScan(force)")]
self.assertIn("clearTimeout(st.scanTimer);st.scanTimer=0", close_scan)
self.assertIn("st.scanEpoch++", close_scan)
start_scan = page[page.index("function startScan(force)"):
page.index("function bars(rssi)")]
self.assertIn("var epoch=++st.scanEpoch", start_scan)
self.assertIn('pollScan(force?"/api/scan?rescan=1":"/api/scan",epoch)',
start_scan)
poll_scan = page[page.index("function pollScan(url,epoch)"):
page.index("function clearInheritedWiFiPassword")]
# Check before issuing a request, after a response, and in the error
# path so neither a stale timer nor a stale reply can alter the panel.
self.assertEqual(poll_scan.count("if(epoch!==st.scanEpoch)return"), 3)
self.assertIn('pollScan("/api/scan",epoch)', poll_scan)
def test_late_config_load_replays_visible_form_edits(self):
page = SOURCE.read_text(encoding="utf-8")
self.assertIn("configLoadCapture:null", page)
self.assertIn("configEpoch:0", page)
listener = page[page.index('document.addEventListener("input"'):
page.index("function updateSaveBar()")]
self.assertIn("el.closest(capture.root)", listener)
self.assertIn("capture.values[el.dataset.k]=elVal(el)", listener)
self.assertIn("capture.confirms[el.dataset.cfm]=el.value", listener)
self.assertIn("capture.wifiPasswordAutoCleared=!!ev.meshAutoWiFiPassword", listener)
self.assertIn("capture.radio[el.dataset.rg]=elVal(el)", listener)
self.assertIn("capture.rxps[el.dataset.rxps]=elVal(el)", listener)
load_config = page[page.index("function loadConfig(loadCapture)"):
page.index("function markDirty")]
self.assertIn("var epoch=++st.configEpoch", load_config)
self.assertIn("loadCapture.loadEpoch=epoch", load_config)
self.assertIn("if(epoch!==st.configEpoch)return false", load_config)
self.assertIn("restoreConfigLoadEdits(loadCapture)", load_config)
self.assertIn("return true", load_config)
restore = page[page.index("function restoreConfigLoadEdits"):
page.index("function rxpsCfgValue")]
self.assertIn('owns.call(capture.values,"wifi.ssid")', restore)
self.assertIn('capture.values["wifi.ssid"]!==st.orig["wifi.ssid"]',
restore)
self.assertIn('!owns.call(capture.values,"wifi.pwd")', restore)
self.assertIn('st.orig["wifi.pwd"]===SENTINEL', restore)
self.assertIn('capture.values["wifi.pwd"]=""', restore)
self.assertIn("capture.wifiPasswordAutoCleared=true", restore)
self.assertIn("st.wifiPasswordAutoCleared=true", restore)
self.assertIn("v!==st.orig[k]", restore)
self.assertIn("if(dirty)st.dirty[k]=v;else delete st.dirty[k]", restore)
self.assertIn("capture.confirms[k]", restore)
self.assertIn("Object.keys(capture.radio)", restore)
self.assertIn("var restoredRadio=radioCombo()", restore)
self.assertIn("if(capture.rxps)", restore)
show_wizard = page[page.index("function showWizard()"):
page.index("function wzGo(n)")]
self.assertLess(show_wizard.index("st.configLoadCapture=capture"),
show_wizard.index("loadConfig(capture)"))
self.assertIn("if(st.configLoadCapture===capture)", show_wizard)
self.assertIn("if(!applied)return", show_wizard)
self.assertIn("st.configEpoch===capture.loadEpoch", show_wizard)
enter_app = page[page.index("function enterApp()"):
page.index("function enterConsole()")]
self.assertIn('root:"#v-app"', enter_app)
self.assertIn("st.configLoadCapture=capture", enter_app)
self.assertIn("loadConfig(capture)", enter_app)
def test_new_ssid_clears_only_inherited_masked_password(self):
page = SOURCE.read_text(encoding="utf-8")
clear_pwd = page[page.index("function clearInheritedWiFiPassword"):
page.index('$("#scan-list").addEventListener')]
self.assertIn('target==="wz-ssid"?"wz-wifi-pwd"', clear_pwd)
self.assertIn('target==="app-ssid"?"app-wifi-pwd"', clear_pwd)
self.assertIn('nextSsid===st.orig["wifi.ssid"]', clear_pwd)
self.assertIn("pwd.value!==SENTINEL", clear_pwd)
self.assertIn('pwd.value=""', clear_pwd)
self.assertIn("st.wifiPasswordAutoCleared=true", clear_pwd)
self.assertIn("st.wifiPasswordAutoCleared=false", clear_pwd)
self.assertIn("pwd.value=SENTINEL", clear_pwd)
self.assertIn("meshAutoWiFiPassword=true", clear_pwd)
choose = page[page.index('$("#scan-list").addEventListener'):
page.index("/* ---------- wizard ---------- */")]
self.assertLess(
choose.index("clearInheritedWiFiPassword(st.scanTarget,selected)"),
choose.index("el.value=selected"),
)
listener = page[page.index('document.addEventListener("input"'):
page.index("function updateSaveBar()")]
self.assertIn(
'if(el.dataset.k==="wifi.ssid")clearInheritedWiFiPassword(el.id,elVal(el))',
listener,
)
def test_password_review_distinguishes_explicit_clear(self):
page = SOURCE.read_text(encoding="utf-8")
review = page[page.index("function buildReview()"):
page.index("function wizardSave()")]
self.assertIn(
'Object.prototype.hasOwnProperty.call(d,"wifi.pwd")', review
)
self.assertIn(
'pwdChanged?(d["wifi.pwd"]?"******":"(open network)")', review
)
def test_bluetooth_address_field_is_capability_gated_and_bound(self):
page = SOURCE.read_text(encoding="utf-8")
self.assertIn('data-cap="65536"><label>Bluetooth address</label>', page)
self.assertIn('data-k="bluetooth.mac"', page)
self.assertIn('case"bluetooth.mac":return r.bluetooth_mac||"";', page)
self.assertIn("random-every-boot", page)
self.assertIn("random-after-connect", page)
self.assertNotIn("random-after-connect, or stealth", page)
self.assertIn('("bluetooth.mac" in setmap)', page)
self.assertIn("Bluetooth address saved [OK] - rebooting", page)
def test_stealth_flag_has_independent_control_and_reboot_flow(self):
page = SOURCE.read_text(encoding="utf-8")
self.assertIn('data-cap="131072"><label>Bluetooth stealth</label>', page)
self.assertIn('data-k="bluetooth.stealth"', page)
self.assertIn('case"bluetooth.stealth":return r.bluetooth_stealth?"on":"off";', page)
self.assertIn('("bluetooth.stealth" in setmap)', page)
self.assertIn('("bluetooth.stealth" in st.dirty)', page)
self.assertIn('("bluetooth.stealth" in submitted)', page)
self.assertIn("Bluetooth stealth saved [OK] - rebooting", page)
self.assertNotIn("|stealth|", page)
def test_scan_panel_exposes_automatic_updates_accessibly(self):
page = SOURCE.read_text(encoding="utf-8")
self.assertIn('id="wz-scan-btn"', page)
self.assertIn('id="app-scan-btn"', page)
self.assertEqual(page.count('aria-controls="scan-panel"'), 2)
self.assertIn('id="scan-panel" role="region"', page)
self.assertIn('id="scan-list" aria-live="polite" aria-busy="true"', page)
open_scan = page[page.index("function openScan(target)"):
page.index("function closeScan()")]
self.assertIn('setAttribute("aria-expanded"', open_scan)
close_scan = page[page.index("function closeScan()"):
page.index("function startScan(force)")]
self.assertIn('setAttribute("aria-hidden","true")', close_scan)
def test_generated_asset_contains_show_controls(self):
header = HEADER.read_text(encoding="utf-8")
self.assertIn("Compressed with Google Zopfli", header)
length = int(re.search(r"WEBCONFIG_HTML_GZ_LEN = (\d+);", header).group(1))
array = header.split(
"const uint8_t WEBCONFIG_HTML_GZ[] PROGMEM = {", 1
)[1]
blob = bytes(
int(value, 16) for value in re.findall(r"0x([0-9a-f]{2})", array)
)[:length]
page = gzip.decompress(blob).decode("utf-8")
self.assertEqual(page.count('onclick="toggleWiFiPassword(this)"'), 2)
self.assertIn("function toggleWiFiPassword(btn)", page)
self.assertIn("function wzSkipRadio()", page)
self.assertIn("function wzSkipMqtt()", page)
self.assertIn("function openWizardScanOnce()", page)
self.assertIn('openScan("wz-ssid")', page)
self.assertIn("function restoreConfigLoadEdits(capture)", page)
self.assertIn("function pollScan(url,epoch)", page)
self.assertIn("function clearInheritedWiFiPassword(target,nextSsid)", page)
self.assertIn('data-cap="65536"><label>Bluetooth address</label>', page)
self.assertIn('data-k="bluetooth.mac"', page)
self.assertIn('case"bluetooth.mac":return r.bluetooth_mac||"";', page)
self.assertIn("random-every-boot", page)
def test_webconfig_serves_the_single_gzip_asset(self):
server = (ROOT / "src/helpers/esp32/WebConfigServer.cpp").read_text(
encoding="utf-8"
)
self.assertIn("WEBCONFIG_HTML_GZ", server)
self.assertIn('res->addHeader("Content-Encoding", "gzip")', server)
self.assertNotIn("WEBCONFIG_HTML_BR", server)
def test_skipped_mqtt_does_not_pin_display_in_setup_mode(self):
main = (ROOT / "examples" / "companion_radio" / "main.cpp").read_text(
encoding="utf-8"
)
display_route = main[main.index("void loop()") : main.index("rtc_clock.tick();")]
self.assertIn("the_mesh.isWebConfigSetupActive()", display_route)
self.assertNotIn("isMQTTConfigured()", display_route)
if __name__ == "__main__":
unittest.main()