From 2373b6f02ef027a423762d5d6349cd9e0f36090e Mon Sep 17 00:00:00 2001 From: cirakg Date: Mon, 19 Oct 2026 22:28:09 +0200 Subject: test(ui): drive the simulator with a scripted flow and check every screen tools/ui_flow.py clicks buttons and taps from the device files, waits for DIAG shown lines and compares 10 screens per watch with goldens on four test and three spot-check devices. Adds the fixture-wrap generator, UI logic tests and driver tests; the launcher opens apps from their glance. Co-Authored-By: Claude Opus 5.5 --- tools/tests/test_gen_fixture_wrap.py | 33 ++++++++++++++++ tools/tests/test_ui_flow.py | 73 ++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 tools/tests/test_gen_fixture_wrap.py create mode 100644 tools/tests/test_ui_flow.py (limited to 'tools/tests') diff --git a/tools/tests/test_gen_fixture_wrap.py b/tools/tests/test_gen_fixture_wrap.py new file mode 100644 index 0000000..1c72e66 --- /dev/null +++ b/tools/tests/test_gen_fixture_wrap.py @@ -0,0 +1,33 @@ +# Author: cirakg & claude ai +# Summary: Tests that the fixture PIN wrap generator matches the constants in source/app/Fixtures.mc and RFC 5869. +from __future__ import annotations + +import re +import shutil +from pathlib import Path + +import gen_fixture_wrap +import pytest + +ROOT = Path(__file__).resolve().parents[2] + + +def fixture_constant(name: str) -> str: + text = (ROOT / "source" / "app" / "Fixtures.mc").read_text(encoding="utf-8") + match = re.search(rf'const {name} = "([0-9a-f]+)"', text) + assert match, f"{name} not found in Fixtures.mc" + return match.group(1) + + +def test_hkdf_matches_rfc5869_case_3() -> None: + # RFC 5869 A.3: empty salt and info. + okm = gen_fixture_wrap.hkdf(bytes([0x0B] * 22), b"", 42) + assert okm.hex() == ( + "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8" # RFC 5869 A.3 + ) + + +@pytest.mark.skipif(shutil.which("openssl") is None, reason="needs openssl for AES") +def test_generated_wrap_matches_the_fixture() -> None: + assert gen_fixture_wrap.fixture_wrap().hex() == fixture_constant("FIXTURE_WRAP") + assert gen_fixture_wrap.MASTER_KEY.hex() == fixture_constant("FIXTURE_MASTER_KEY") diff --git a/tools/tests/test_ui_flow.py b/tools/tests/test_ui_flow.py new file mode 100644 index 0000000..7e0d336 --- /dev/null +++ b/tools/tests/test_ui_flow.py @@ -0,0 +1,73 @@ +# Author: cirakg & claude ai +# Summary: Tests for the UI flow driver's pure parts: device key layout, JSON lookup and the app-log reader. +from __future__ import annotations + +import json +import threading +import time +from pathlib import Path + +import pytest +import ui_flow + + +def write_sim(tmp_path: Path, config: dict[str, object]) -> Path: + (tmp_path / "simulator.json").write_text(json.dumps(config), encoding="utf-8") + return tmp_path + + +def test_load_keys_gives_button_centres(tmp_path: Path) -> None: + device = write_sim( + tmp_path, + { + "keys": [ + {"id": "enter", "behavior": "onSelect", "location": {"x": 10, "y": 20, "width": 30, "height": 40}}, + { + "id": "menu", + "behavior": "onMenu", + "isHold": True, + "location": {"x": 0, "y": 0, "width": 2, "height": 2}, + }, + {"id": "start", "location": {"x": 5, "y": 5, "width": 10, "height": 10}}, + ] + }, + ) + keys = ui_flow.load_keys(device) + assert keys["enter"] == ui_flow.Key(25, 40, "onSelect", False) + assert keys["menu"].hold + assert keys["start"].behavior == "" + + +def test_find_key_searches_nested_structures() -> None: + data = {"display": {"isTouch": True}, "glance": [{"contentArea": {"x": 1}}]} + assert ui_flow.find_key(data, "isTouch") is True + assert ui_flow.find_key(data, "contentArea") == {"x": 1} + assert ui_flow.find_key(data, "missing") is None + + +def test_app_log_waits_only_for_lines_after_the_mark(tmp_path: Path) -> None: + log_path = tmp_path / "app.log" + log_path.write_text("DIAG shown list\n", encoding="utf-8") + log = ui_flow.AppLog(log_path) + assert log.wait_for(r"DIAG shown list\s*$", 1).group(0).startswith("DIAG shown list") + log.set_mark() + + def append_later() -> None: + time.sleep(0.2) + with log_path.open("a", encoding="utf-8") as f: + f.write("DIAG shown otp\n") + + writer = threading.Thread(target=append_later) + writer.start() + assert log.wait_for(r"DIAG shown otp", 3) + writer.join() + with pytest.raises(ui_flow.FlowError): + log.wait_for(r"DIAG shown list", 0.3) # only before the mark + + +def test_app_log_latest_returns_the_last_match(tmp_path: Path) -> None: + log_path = tmp_path / "app.log" + log_path.write_text("DIAG list_focus y=10\nother\nDIAG list_focus y=42\n", encoding="utf-8") + match = ui_flow.AppLog(log_path).latest(r"DIAG list_focus y=(\d+)") + assert match is not None and match.group(1) == "42" + assert ui_flow.AppLog(tmp_path / "missing.log").latest("x") is None -- cgit v1.2.3