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>
98 lines
4.1 KiB
Python
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")
|