Files
BT411/scratchpad/test_controls_page.py
T
arcattackandClaude Opus 5 e46fc706a6 CONTROLS.html: load your bindings.txt and the board becomes YOUR board
The interactive controls page was a static picture of the stock layout -- a
player's carried migration rows, rebinds and HOTAS setup were invisible to it,
and the mismatch grew with every customization.  Now a "Load your bindings.txt"
box (file picker + drag-drop) parses the real bindings grammar client-side and
rewrites the interactive keyboard in place:

  * every key shows the loaded file's truth; keys that differ from the stock
    board get a hazard outline (the legend says so); hover readouts follow
    because they were already attribute-driven;
  * the game's bindings-row-wins rule is modelled: the PgUp/PgDn volume and
    backtick view BUILT-INS keep their face only while their key is unbound;
  * rows that have no keyboard geometry (pad / padaxis / wizard joyaxis-
    joybutton-joyhat / MOUSE keys) land in an "also in your file" list;
  * one click restores the stock view.  Nothing is uploaded -- FileReader on a
    user-chosen local file, works from the extracted zip on file://.

Key-name registry walks the three key blocks in DOM order (Shift/Ctrl/Alt
resolve left-then-right; the numpad's U+2212 minus and its ASCII twin both map
to NUMPADMINUS), address meanings are harvested from the stock board itself
plus a small table for the slots the default board leaves off (unwired columns,
the cabinet-only throttle bank, PANIC).

Two self-inflicted bugs found by the harness and fixed before landing: the
address harvest invented a "BTN" label for the many stock keys that have none,
which made EVERY default row read as a customization; and generated axis labels
("AIM +") differed from the page's hand-authored vocabulary ("AIM UP"), custom-
marking the whole stock numpad.  Both now speak the board's own labels.

VERIFIED headlessly (no browser extension needed): scratchpad/
test_controls_page.py injects a self-test harness into a copy of the page,
feeds it scratchpad/fixture_migrated_bindings.txt -- a REAL board-2 migrated
file (full defaults + two authored rows + a wizard section) -- and runs it
under chrome --headless=new --dump-dom.  11/11: the two authored rows carried
and marked, zero stray custom marks across the rest of the board, both
built-ins survive, wizard rows listed, status line honest, and reset restores
the stock board exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:47:12 -05:00

98 lines
4.1 KiB
Python

"""Headless test for CONTROLS.html's bindings-loader.
Builds a copy of the page with a self-test harness injected inside its IIFE,
feeds it the fixture (a REAL board-2 migrated bindings.txt: the full defaults
plus two player-authored rows and a wizard section), and writes the result to
%TEMP%\controls_test.html. Run it under headless Chrome and read the
PASS/FAIL lines out of the dumped DOM's <pre id="test-out">:
python scratchpad/test_controls_page.py
chrome --headless=new --disable-gpu --dump-dom file:///%TEMP%/controls_test.html
Asserts: player-authored rows are carried and marked custom, stock rows stay
unmarked, the PgUp/PgDn and backtick built-ins survive unbound keys, wizard
rows land in the extras list, and reset restores the stock board exactly.
Verified 11/11 on 2026-07-26.
"""
import io
import json
import os
import tempfile
SCRATCH = tempfile.gettempdir()
frag = io.open(r"C:\git\bt411\docs\CONTROLS.html", encoding="utf-8").read()
sample = io.open(r"C:\git\bt411\scratchpad\fixture_migrated_bindings.txt",
encoding="utf-8", errors="replace").read()
harness = r"""
// ---------- INJECTED SELF-TEST (test copy only) ----------
try {
var SAMPLE = __SAMPLE__;
applyFile(SAMPLE, "migrated-test");
var out = [];
function t(label, ok) { out.push((ok ? "PASS" : "FAIL") + " " + label); }
function elOf(n) { return keyEls[n]; }
function subOf(n) {
var s = elOf(n) && elOf(n).querySelector(".k-sub");
return s ? s.textContent : "";
}
t("T carries the custom button (VALVE face from 0x2B)",
subOf("T") === "VALVE" && elOf("T").classList.contains("custom"));
t("Q carries the tweaked throttle (THR +) and is custom",
subOf("Q") === "THR +" && elOf("Q").classList.contains("custom"));
t("W matches stock (VALVE, not custom)",
subOf("W") === "VALVE" && !elOf("W").classList.contains("custom"));
t("PgUp built-in volume survives an unbound key",
subOf("PAGEUP") === "VOL +"
&& !elOf("PAGEUP").classList.contains("custom"));
t("backtick view built-in survives", subOf("BACKTICK") === "VIEW");
t("extras list shows the wizard rows",
document.getElementById("load-extras").style.display === "block"
&& document.getElementById("load-extras")
.textContent.indexOf("joyaxis") >= 0);
t("status reports a load",
document.getElementById("load-status")
.textContent.indexOf("differ from stock") >= 0);
var stray = 0, strayList = [];
Object.keys(keyEls).forEach(function (n) {
if (n !== "T" && n !== "Q"
&& keyEls[n].classList.contains("custom")) {
stray += 1;
var sEl = keyEls[n].querySelector(".k-sub");
strayList.push(n + "[sub='" + (sEl ? sEl.textContent : "")
+ "' stockSub='" + stock[n].sub + "' addr='"
+ keyEls[n].getAttribute("data-addr") + "' stockAddr='"
+ stock[n].addr + "']");
}
});
t("no stray custom marks (only T and Q); strays: "
+ strayList.join(" "), stray === 0);
resetBoard();
t("reset restores W", subOf("W") === "VALVE");
t("reset restores Q to stock VALVE", subOf("Q") === "VALVE");
t("reset clears custom marks",
document.querySelectorAll(".key.custom").length === 0);
var pre = document.createElement("pre");
pre.id = "test-out";
pre.textContent = out.join("\n");
document.body.appendChild(pre);
} catch (err) {
var pre2 = document.createElement("pre");
pre2.id = "test-out";
pre2.textContent = "HARNESS ERROR: " + err + " :: " + (err && err.stack);
document.body.appendChild(pre2);
}
"""
harness = harness.replace("__SAMPLE__", json.dumps(sample))
closer = " })();\n</script>"
assert closer in frag, "closer not found"
test = frag.replace(closer, harness + closer, 1)
page = ("<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n"
"<title>test</title>\n</head>\n<body>\n" + test + "\n</body>\n</html>\n")
out = os.path.join(SCRATCH, "controls_test.html")
io.open(out, "w", encoding="utf-8").write(page)
print("test page written:", len(page), "chars")