feat(web): Rotation, Operation History, Config Editor and Backup & Restore become ES-module pages (stage 2) (#727)

Rotation, Operation History, Config Editor and Backup & Restore become ES-module pages (stage 2): no inline scripts or onclick in the four partials, delegated data-action listeners, reads cancelled on swap-out, old globals kept as deprecated aliases through window.LEDMatrix, and four new DOM suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-02 18:02:03 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent c14002edc3
commit a7b3f33952
22 changed files with 2019 additions and 819 deletions
+67 -4
View File
@@ -41,7 +41,9 @@ def _url(path):
def test_the_module_directories_hold_modules():
assert {p.name for p in MODULES} >= {"boot.js", "registry.js", "api.js", "facade.js", "cache.js"}
assert {p.name for p in MODULES} >= {"boot.js", "registry.js", "api.js", "facade.js", "cache.js",
"durations.js", "operation-history.js", "raw-json.js",
"backup-restore.js"}
for directory in MODULE_DIRS:
# node needs this to import them in the JS tests; browsers ignore it.
assert '"type": "module"' in (directory / "package.json").read_text(encoding="utf-8")
@@ -94,10 +96,71 @@ def test_every_import_resolves_inside_the_module_tree(module):
f"{module.name}: {spec!r} leaves js/core and js/pages")
# Converted so far: page name -> (its partial, the route that serves it). The
# migration order is in docs/WEB_FRONTEND_ARCHITECTURE.md.
CONVERTED = {
"cache": ("cache.html", "/partials/cache"),
"durations": ("durations.html", "/partials/durations"),
"operation-history": ("operation_history.html", "/partials/operation-history"),
"raw-json": ("raw_json.html", "/partials/raw-json"),
"backup-restore": ("backup_restore.html", "/partials/backup-restore"),
}
# Old window.* names that moved into a page module. Each stays as a
# deprecated alias in boot.js that forwards to the module's export.
ALIASES = {
"cache": ["deleteCacheFile"],
"raw-json": ["formatJson", "manualValidateJson", "validateJSON",
"saveMainConfig", "saveSecretsConfig"],
"backup-restore": ["exportBackup", "loadBackupList", "validateRestoreFile",
"clearRestore", "runRestore"],
}
def _boot():
return (JS / "core" / "boot.js").read_text(encoding="utf-8")
def _registered_pages():
boot = (JS / "core" / "boot.js").read_text(encoding="utf-8")
return re.findall(r"registry\.register\('([\w-]+)',\s*function\(\)\s*\{\s*return import\('\.\./pages/([\w-]+)\.js'\)",
boot)
return re.findall(r"'([\w-]+)':\s*page\(function\(\)\s*\{\s*return import\('\.\./pages/([\w-]+)\.js'\)",
_boot())
def test_every_converted_page_is_registered():
assert dict(_registered_pages()) == {name: name for name in CONVERTED}
@pytest.mark.parametrize("name", sorted(CONVERTED))
def test_a_converted_partial_roots_its_page(client, name):
partial, route = CONVERTED[name]
text = (PARTIALS / partial).read_text(encoding="utf-8")
assert f'data-page="{name}"' in text
assert "<script" not in text.lower()
# Its buttons are wired by the module, not by inline handlers naming
# globals (which would reach the code only through a deprecated alias).
assert "onclick=" not in text.lower()
module = (JS / "pages" / f"{name}.js").read_text(encoding="utf-8")
assert re.search(r"^export function init\(root, ctx\)", module, re.M)
# Rendered by the real route, the root is there exactly once.
resp = client.get(route)
assert resp.status_code == 200, route
assert resp.get_data(as_text=True).count(f'data-page="{name}"') == 1, route
@pytest.mark.parametrize("name", sorted(ALIASES))
def test_moved_globals_stay_as_aliases(name):
boot = _boot()
module = (JS / "pages" / f"{name}.js").read_text(encoding="utf-8")
for global_name in ALIASES[name]:
assert f"'{global_name}'" in boot, f"boot.js does not alias {global_name}"
assert re.search(rf"^export (?:function|const) {global_name}\b", module, re.M), (
f"pages/{name}.js does not export {global_name}")
# No template defines them any more.
for partial in PARTIALS.glob("*.html"):
text = partial.read_text(encoding="utf-8")
for global_name in ALIASES[name]:
assert f"window.{global_name} =" not in text, partial.name
assert f"function {global_name}(" not in text, partial.name
def test_every_registered_page_has_its_module_and_partial():