#!/usr/bin/env python3
"""Optional real-browser checks for WebConfig's asynchronous setup races."""
import gzip
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
HEADER = ROOT / "src" / "helpers" / "esp32" / "WebConfigHtml.h"
def chromium_path():
candidates = [
shutil.which(name)
for name in ("google-chrome", "chromium", "chromium-browser", "chrome", "msedge")
]
for base, rest in (
(os.environ.get("ProgramFiles"), ("Google", "Chrome", "Application", "chrome.exe")),
(os.environ.get("ProgramFiles(x86)"), ("Microsoft", "Edge", "Application", "msedge.exe")),
(os.environ.get("LocalAppData"), ("Google", "Chrome", "Application", "chrome.exe")),
):
if base:
candidates.append(str(Path(base).joinpath(*rest)))
return next((path for path in candidates if path and Path(path).is_file()), None)
BROWSER = chromium_path()
def embedded_page():
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]
return gzip.decompress(blob).decode("utf-8")
class WebConfigUiRuntimeTest(unittest.TestCase):
def setup_values(self):
sys.path.insert(0, str(ROOT / "scripts"))
try:
import webconfig_mock_server as mock
finally:
sys.path.pop(0)
config = mock.default_config(True)
config["wifi"]["ssid"] = "StoredNet"
config["wifi"]["pwd"] = "********"
status = {
"mode": "setup",
"auth": True,
"mqtt": False,
"cli": False,
"wifi_psk64": True,
"password_supported": True,
"needs_password": False,
"radio_optional": True,
"needs_setup": False,
"capabilities": 0,
"max_cmds": 16,
"name": "runtime-test",
"role": "Companion",
"board": "mock",
"fw": "v1.17.1-test",
"build_date": "runtime",
}
return status, config
def run_page(self, prelude, virtual_time=1500):
if not BROWSER:
self.skipTest("Chromium-family browser is unavailable")
source = embedded_page()
markers = ('
""" % (json.dumps(status), json.dumps(config), json.dumps(explicit_password))
return self.run_page(prelude)
def test_early_new_ssid_clears_password_loaded_later(self):
dom = self.run_early_selection_case()
self.assertIn('data-test-ssid="EarlyNet"', dom)
self.assertIn('data-test-orig-pwd="********"', dom)
self.assertIn('data-test-wz-pwd=""', dom)
self.assertIn('data-test-app-pwd=""', dom)
self.assertIn('data-test-dirty-ssid="EarlyNet"', dom)
self.assertIn('data-test-dirty-pwd-present="yes"', dom)
self.assertIn('data-test-dirty-pwd=""', dom)
def test_early_explicit_password_is_preserved(self):
dom = self.run_early_selection_case("new-secret")
self.assertIn('data-test-ssid="EarlyNet"', dom)
self.assertIn('data-test-wz-pwd="new-secret"', dom)
self.assertIn('data-test-app-pwd="new-secret"', dom)
self.assertIn('data-test-dirty-pwd-present="yes"', dom)
self.assertIn('data-test-dirty-pwd="new-secret"', dom)
def test_setup_automatically_opens_embedded_scan_picker(self):
status, config = self.setup_values()
prelude = """
""" % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
self.assertIn('data-test-scan-open="yes"', dom)
self.assertIn('data-test-scan-hidden="false"', dom)
self.assertIn('data-test-scan-expanded="true"', dom)
self.assertIn('data-test-scan-busy="false"', dom)
self.assertIn('data-test-network-count="1"', dom)
self.assertIn('data-test-ssid="StoredNet"', dom)
self.assertIn('data-test-pwd="********"', dom)
self.assertIn('data-test-dirty-count="0"', dom)
def test_typed_new_ssid_clears_inherited_password_and_review(self):
status, config = self.setup_values()
prelude = """
""" % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
self.assertIn('data-test-ssid="TypedNet"', dom)
self.assertIn('data-test-pwd=""', dom)
self.assertIn('data-test-dirty-pwd="yes"', dom)
self.assertIn('data-test-review-open="yes"', dom)
def test_returning_to_original_ssid_restores_masked_password(self):
status, config = self.setup_values()
prelude = """
""" % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
self.assertIn('data-test-pwd="********"', dom)
self.assertIn('data-test-dirty-ssid="no"', dom)
self.assertIn('data-test-dirty-pwd="no"', dom)
self.assertIn('data-test-auto-cleared="no"', dom)
def test_advanced_editor_edit_during_config_load_is_preserved(self):
status, config = self.setup_values()
status["mode"] = "lan"
prelude = """
""" % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
self.assertIn('data-test-name="Early App Edit"', dom)
self.assertIn('data-test-dirty-name="Early App Edit"', dom)
self.assertIn('data-test-capture="clear"', dom)
def test_second_load_auto_password_clear_remains_restorable(self):
status, config = self.setup_values()
prelude = """
""" % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
self.assertIn('data-test-restored-pwd="********"', dom)
self.assertIn('data-test-restored-dirty-ssid="no"', dom)
self.assertIn('data-test-restored-dirty-pwd="no"', dom)
self.assertIn('data-test-restored-auto="no"', dom)
self.assertIn('data-test-manual-pwd="manual-secret"', dom)
self.assertIn('data-test-manual-dirty-ssid="no"', dom)
self.assertIn('data-test-manual-dirty-pwd="yes"', dom)
self.assertIn('data-test-manual-auto="no"', dom)
self.assertIn('data-test-config-calls="2"', dom)
self.assertIn('data-test-capture="clear"', dom)
def test_delayed_app_load_preserves_only_edited_radio_field(self):
status, config = self.setup_values()
status["mode"] = "lan"
prelude = """
""" % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
self.assertIn('data-test-radio-freq="910.525"', dom)
self.assertIn('data-test-radio-bw="62.5"', dom)
self.assertIn('data-test-radio-sf="9"', dom)
self.assertIn('data-test-radio-cr="5"', dom)
self.assertIn('data-test-radio-orig="910.525,62.5,7,5"', dom)
self.assertIn('data-test-radio-dirty="910.525,62.5,9,5"', dom)
self.assertIn('data-test-capture="clear"', dom)
def test_closed_picker_ignores_late_scan_response(self):
status, config = self.setup_values()
prelude = """
""" % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
self.assertIn('data-test-scan-hidden="true"', dom)
self.assertIn('data-test-scan-expanded="false"', dom)
self.assertIn('data-test-network-count="0"', dom)
self.assertIn('data-test-scan-text="closed"', dom)
def test_newer_config_load_wins_when_older_reply_finishes_last(self):
status, old_config = self.setup_values()
new_config = json.loads(json.dumps(old_config))
new_config["wifi"]["ssid"] = "AppNet"
prelude = """
""" % (json.dumps(status), json.dumps(old_config), json.dumps(new_config))
dom = self.run_page(prelude)
self.assertIn('data-test-config-calls="2"', dom)
self.assertIn('data-test-orig-ssid="AppNet"', dom)
self.assertIn('data-test-field-ssid="AppNet"', dom)
self.assertIn('data-test-scan-hidden="true"', dom)
self.assertIn('data-test-capture="clear"', dom)
if __name__ == "__main__":
unittest.main()