#!/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', 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', page) self.assertIn('onclick="startScan(true)">Rescan', 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">', 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">', 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") 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">', 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_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()