host.controller_web_editor.tests.test_hitboxes_route

Round-trip + safety tests for the hitbox layout route.

Generated by Claude

Verifies the layout file loads cleanly, the layout allowlist rejects unknown names, and save_layout writes a recoverable .bak.

  1"""Round-trip + safety tests for the hitbox layout route.
  2
  3# Generated by Claude
  4
  5Verifies the layout file loads cleanly, the layout allowlist rejects
  6unknown names, and save_layout writes a recoverable .bak.
  7"""
  8
  9import json
 10import os
 11import sys
 12
 13import pytest
 14
 15
 16_REPO_ROOT = os.path.abspath(
 17    os.path.join(os.path.dirname(__file__), "..", "..", ".."))
 18if _REPO_ROOT not in sys.path:
 19    sys.path.insert(0, _REPO_ROOT)
 20
 21
 22from host.controller_web_editor.routes.hitboxes import (  # noqa: E402
 23    HITBOXES_DIR,
 24    LayoutNotAllowedError,
 25    list_layouts,
 26    load_layout,
 27    save_layout,
 28)
 29
 30
 31class TestLoad:
 32    def test_xbox_layout_loads(self):
 33        data = load_layout("xbox")
 34        assert data["viewBox"] == "0 0 744 500"
 35        assert isinstance(data["regions"], dict)
 36        # Spot-check that the canonical xbox inputs are present.
 37        assert "a_button" in data["regions"]
 38        assert "left_trigger" in data["regions"]
 39        assert "pov_up" in data["regions"]
 40
 41    def test_rejects_unknown_layout(self):
 42        with pytest.raises(LayoutNotAllowedError):
 43            load_layout("ps5")
 44
 45    def test_rejects_traversal(self):
 46        with pytest.raises(LayoutNotAllowedError):
 47            load_layout("../paths")
 48
 49    def test_list_layouts_contains_xbox(self):
 50        assert "xbox" in list_layouts()
 51
 52
 53class TestSave:
 54    @pytest.fixture
 55    def restore_xbox(self):
 56        # Snapshot the file so the test can restore it regardless of outcome.
 57        src = HITBOXES_DIR / "xbox.json"
 58        snapshot = src.read_bytes()
 59        yield
 60        src.write_bytes(snapshot)
 61        bak = src.with_suffix(src.suffix + ".bak")
 62        if bak.exists():
 63            bak.unlink()
 64
 65    def test_save_writes_file_and_bak(self, restore_xbox):
 66        original = load_layout("xbox")
 67        # Tweak one field so we can confirm the write took.
 68        modified = json.loads(json.dumps(original))
 69        modified["regions"]["a_button"] = {
 70            "shape": "circle", "cx": 999, "cy": 999, "r": 17,
 71        }
 72        save_layout("xbox", modified)
 73
 74        reloaded = load_layout("xbox")
 75        assert reloaded["regions"]["a_button"]["cx"] == 999
 76
 77        bak = HITBOXES_DIR / "xbox.json.bak"
 78        assert bak.is_file()
 79        assert json.loads(bak.read_text())["regions"]["a_button"]["cx"] == \
 80            original["regions"]["a_button"]["cx"]
 81
 82    def test_save_rejects_unknown_layout(self):
 83        with pytest.raises(LayoutNotAllowedError):
 84            save_layout("ps5", {"viewBox": "0 0 1 1", "regions": {}})
 85
 86    def test_save_rejects_missing_regions(self, restore_xbox):
 87        with pytest.raises(ValueError):
 88            save_layout("xbox", {"viewBox": "0 0 1 1"})
 89
 90    def test_save_rejects_missing_viewbox(self, restore_xbox):
 91        with pytest.raises(ValueError):
 92            save_layout("xbox", {"regions": {}})
 93
 94    def test_save_rejects_non_dict(self, restore_xbox):
 95        with pytest.raises(ValueError):
 96            save_layout("xbox", ["not", "a", "dict"])  # type: ignore[arg-type]
 97
 98    def test_save_accepts_optional_labels_and_fonts(self, restore_xbox):
 99        original = load_layout("xbox")
100        payload = json.loads(json.dumps(original))
101        payload["labels"] = {"a_button": {"dx": 4.5, "dy": -22.0}}
102        payload["fonts"] = {"input_name": 8, "action_label": 12}
103        save_layout("xbox", payload)
104        reloaded = load_layout("xbox")
105        assert reloaded["labels"]["a_button"]["dy"] == -22.0
106        assert reloaded["fonts"]["action_label"] == 12
107
108    def test_save_rejects_non_dict_labels(self, restore_xbox):
109        original = load_layout("xbox")
110        payload = json.loads(json.dumps(original))
111        payload["labels"] = "not a dict"
112        with pytest.raises(ValueError):
113            save_layout("xbox", payload)
114
115    def test_save_rejects_non_dict_fonts(self, restore_xbox):
116        original = load_layout("xbox")
117        payload = json.loads(json.dumps(original))
118        payload["fonts"] = 12
119        with pytest.raises(ValueError):
120            save_layout("xbox", payload)
class TestLoad:
32class TestLoad:
33    def test_xbox_layout_loads(self):
34        data = load_layout("xbox")
35        assert data["viewBox"] == "0 0 744 500"
36        assert isinstance(data["regions"], dict)
37        # Spot-check that the canonical xbox inputs are present.
38        assert "a_button" in data["regions"]
39        assert "left_trigger" in data["regions"]
40        assert "pov_up" in data["regions"]
41
42    def test_rejects_unknown_layout(self):
43        with pytest.raises(LayoutNotAllowedError):
44            load_layout("ps5")
45
46    def test_rejects_traversal(self):
47        with pytest.raises(LayoutNotAllowedError):
48            load_layout("../paths")
49
50    def test_list_layouts_contains_xbox(self):
51        assert "xbox" in list_layouts()
def test_xbox_layout_loads(self):
33    def test_xbox_layout_loads(self):
34        data = load_layout("xbox")
35        assert data["viewBox"] == "0 0 744 500"
36        assert isinstance(data["regions"], dict)
37        # Spot-check that the canonical xbox inputs are present.
38        assert "a_button" in data["regions"]
39        assert "left_trigger" in data["regions"]
40        assert "pov_up" in data["regions"]
def test_rejects_unknown_layout(self):
42    def test_rejects_unknown_layout(self):
43        with pytest.raises(LayoutNotAllowedError):
44            load_layout("ps5")
def test_rejects_traversal(self):
46    def test_rejects_traversal(self):
47        with pytest.raises(LayoutNotAllowedError):
48            load_layout("../paths")
def test_list_layouts_contains_xbox(self):
50    def test_list_layouts_contains_xbox(self):
51        assert "xbox" in list_layouts()
class TestSave:
 54class TestSave:
 55    @pytest.fixture
 56    def restore_xbox(self):
 57        # Snapshot the file so the test can restore it regardless of outcome.
 58        src = HITBOXES_DIR / "xbox.json"
 59        snapshot = src.read_bytes()
 60        yield
 61        src.write_bytes(snapshot)
 62        bak = src.with_suffix(src.suffix + ".bak")
 63        if bak.exists():
 64            bak.unlink()
 65
 66    def test_save_writes_file_and_bak(self, restore_xbox):
 67        original = load_layout("xbox")
 68        # Tweak one field so we can confirm the write took.
 69        modified = json.loads(json.dumps(original))
 70        modified["regions"]["a_button"] = {
 71            "shape": "circle", "cx": 999, "cy": 999, "r": 17,
 72        }
 73        save_layout("xbox", modified)
 74
 75        reloaded = load_layout("xbox")
 76        assert reloaded["regions"]["a_button"]["cx"] == 999
 77
 78        bak = HITBOXES_DIR / "xbox.json.bak"
 79        assert bak.is_file()
 80        assert json.loads(bak.read_text())["regions"]["a_button"]["cx"] == \
 81            original["regions"]["a_button"]["cx"]
 82
 83    def test_save_rejects_unknown_layout(self):
 84        with pytest.raises(LayoutNotAllowedError):
 85            save_layout("ps5", {"viewBox": "0 0 1 1", "regions": {}})
 86
 87    def test_save_rejects_missing_regions(self, restore_xbox):
 88        with pytest.raises(ValueError):
 89            save_layout("xbox", {"viewBox": "0 0 1 1"})
 90
 91    def test_save_rejects_missing_viewbox(self, restore_xbox):
 92        with pytest.raises(ValueError):
 93            save_layout("xbox", {"regions": {}})
 94
 95    def test_save_rejects_non_dict(self, restore_xbox):
 96        with pytest.raises(ValueError):
 97            save_layout("xbox", ["not", "a", "dict"])  # type: ignore[arg-type]
 98
 99    def test_save_accepts_optional_labels_and_fonts(self, restore_xbox):
100        original = load_layout("xbox")
101        payload = json.loads(json.dumps(original))
102        payload["labels"] = {"a_button": {"dx": 4.5, "dy": -22.0}}
103        payload["fonts"] = {"input_name": 8, "action_label": 12}
104        save_layout("xbox", payload)
105        reloaded = load_layout("xbox")
106        assert reloaded["labels"]["a_button"]["dy"] == -22.0
107        assert reloaded["fonts"]["action_label"] == 12
108
109    def test_save_rejects_non_dict_labels(self, restore_xbox):
110        original = load_layout("xbox")
111        payload = json.loads(json.dumps(original))
112        payload["labels"] = "not a dict"
113        with pytest.raises(ValueError):
114            save_layout("xbox", payload)
115
116    def test_save_rejects_non_dict_fonts(self, restore_xbox):
117        original = load_layout("xbox")
118        payload = json.loads(json.dumps(original))
119        payload["fonts"] = 12
120        with pytest.raises(ValueError):
121            save_layout("xbox", payload)
@pytest.fixture
def restore_xbox(self):
55    @pytest.fixture
56    def restore_xbox(self):
57        # Snapshot the file so the test can restore it regardless of outcome.
58        src = HITBOXES_DIR / "xbox.json"
59        snapshot = src.read_bytes()
60        yield
61        src.write_bytes(snapshot)
62        bak = src.with_suffix(src.suffix + ".bak")
63        if bak.exists():
64            bak.unlink()
def test_save_writes_file_and_bak(self, restore_xbox):
66    def test_save_writes_file_and_bak(self, restore_xbox):
67        original = load_layout("xbox")
68        # Tweak one field so we can confirm the write took.
69        modified = json.loads(json.dumps(original))
70        modified["regions"]["a_button"] = {
71            "shape": "circle", "cx": 999, "cy": 999, "r": 17,
72        }
73        save_layout("xbox", modified)
74
75        reloaded = load_layout("xbox")
76        assert reloaded["regions"]["a_button"]["cx"] == 999
77
78        bak = HITBOXES_DIR / "xbox.json.bak"
79        assert bak.is_file()
80        assert json.loads(bak.read_text())["regions"]["a_button"]["cx"] == \
81            original["regions"]["a_button"]["cx"]
def test_save_rejects_unknown_layout(self):
83    def test_save_rejects_unknown_layout(self):
84        with pytest.raises(LayoutNotAllowedError):
85            save_layout("ps5", {"viewBox": "0 0 1 1", "regions": {}})
def test_save_rejects_missing_regions(self, restore_xbox):
87    def test_save_rejects_missing_regions(self, restore_xbox):
88        with pytest.raises(ValueError):
89            save_layout("xbox", {"viewBox": "0 0 1 1"})
def test_save_rejects_missing_viewbox(self, restore_xbox):
91    def test_save_rejects_missing_viewbox(self, restore_xbox):
92        with pytest.raises(ValueError):
93            save_layout("xbox", {"regions": {}})
def test_save_rejects_non_dict(self, restore_xbox):
95    def test_save_rejects_non_dict(self, restore_xbox):
96        with pytest.raises(ValueError):
97            save_layout("xbox", ["not", "a", "dict"])  # type: ignore[arg-type]
def test_save_accepts_optional_labels_and_fonts(self, restore_xbox):
 99    def test_save_accepts_optional_labels_and_fonts(self, restore_xbox):
100        original = load_layout("xbox")
101        payload = json.loads(json.dumps(original))
102        payload["labels"] = {"a_button": {"dx": 4.5, "dy": -22.0}}
103        payload["fonts"] = {"input_name": 8, "action_label": 12}
104        save_layout("xbox", payload)
105        reloaded = load_layout("xbox")
106        assert reloaded["labels"]["a_button"]["dy"] == -22.0
107        assert reloaded["fonts"]["action_label"] == 12
def test_save_rejects_non_dict_labels(self, restore_xbox):
109    def test_save_rejects_non_dict_labels(self, restore_xbox):
110        original = load_layout("xbox")
111        payload = json.loads(json.dumps(original))
112        payload["labels"] = "not a dict"
113        with pytest.raises(ValueError):
114            save_layout("xbox", payload)
def test_save_rejects_non_dict_fonts(self, restore_xbox):
116    def test_save_rejects_non_dict_fonts(self, restore_xbox):
117        original = load_layout("xbox")
118        payload = json.loads(json.dumps(original))
119        payload["fonts"] = 12
120        with pytest.raises(ValueError):
121            save_layout("xbox", payload)