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 --- docker/scripts/build-app.sh | 14 ++++++++------ docker/scripts/ciq-env.sh | 29 +++++++++++++++++++++++++---- docker/scripts/screenshots.sh | 31 ++++++++++++++++--------------- 3 files changed, 49 insertions(+), 25 deletions(-) (limited to 'docker/scripts') diff --git a/docker/scripts/build-app.sh b/docker/scripts/build-app.sh index 74dd042..1a7d59a 100755 --- a/docker/scripts/build-app.sh +++ b/docker/scripts/build-app.sh @@ -1,20 +1,21 @@ #!/usr/bin/env bash # Author: cirakg & claude ai -# Summary: Compile the watch app for one device: debug, release or unit-test build. +# Summary: Compile the watch app for one device: debug, release, unit-test or UI-test build. # -# Usage: build-app.sh debug|release|test DEVICE +# Usage: build-app.sh debug|release|test|uitest DEVICE # Output: build//.prg, and the compiler log next to it. # # Policy (README section 17.7): strict type checking (-l 3), and any compiler -# warning fails the build. Test builds add test.jungle, so test code never +# warning fails the build. UI-test builds add uitest.jungle (fixed clock, +# fake entries; see source/util/TestMode.mc). Test builds add test.jungle, so test code never # reaches debug or release builds; release builds (-r) also drop every # (:debug) declaration, such as the diagnostics in source/util/Diag.mc. set -euo pipefail # shellcheck source=ciq-env.sh source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" -mode="${1:?usage: build-app.sh debug|release|test DEVICE}" -device="${2:?usage: build-app.sh debug|release|test DEVICE}" +mode="${1:?usage: build-app.sh debug|release|test|uitest DEVICE}" +device="${2:?usage: build-app.sh debug|release|test|uitest DEVICE}" ciq_require_sdk ciq_require_key @@ -27,11 +28,12 @@ args=(-d "${device}" -y "${DEV_KEY}" -l 3 -w) case "${mode}" in debug) ;; release) args+=(-r) ;; + uitest) jungles="${jungles};/work/uitest.jungle" ;; test) jungles="${jungles};/work/test.jungle" args+=(-t) ;; - *) die "unknown build mode '${mode}' (use debug, release or test)" ;; + *) die "unknown build mode '${mode}' (use debug, release, test or uitest)" ;; esac out_dir="${BUILD_DIR}/${mode}" diff --git a/docker/scripts/ciq-env.sh b/docker/scripts/ciq-env.sh index a52db78..6216321 100755 --- a/docker/scripts/ciq-env.sh +++ b/docker/scripts/ciq-env.sh @@ -87,6 +87,9 @@ ciq_start_simulator() { # often reuses that process ID for something else, and the simulator then # believes it is already running and never opens its port. Each container # runs exactly one simulator, so the stale lock is safe to remove. + # A simulator this container already started is reused: a second one + # would open a second window, and input could go to the wrong one. + ciq_simulator_listening && return 0 rm -f "${HOME}"/Sim-* simulator >/tmp/simulator.log 2>&1 & local pid=$! _ @@ -109,19 +112,37 @@ ciq_simulator_window() { # Longest wait for an app's first frame after launching it. FIRST_FRAME_TIMEOUT_S=60 -# Launch the debug build of the app on DEVICE and wait for its first frame, +# Launch the app on DEVICE (the debug build, or MODE's build: uitest) and +# wait for its first frame, # which the app reports with a "DIAG first_frame" console line. The console # goes to LOG, followed by a "HARNESS first_frame wall_ms=N" line: the # wall-clock time from launch to first frame, including the JVM start of # monkeydo and the program load the app can't time itself. The app keeps # running (for screenshots) until ciq_stop_app or the next launch. -# Returns 1 if no first frame appeared in time. +# Apps with a glance start in it; the glance is opened with START or a tap, +# so the wall time includes that. Returns 1 if no first frame appeared in time. ciq_launch_app() { - local device="$1" app_log="$2" start _ + local device="$1" app_log="$2" mode="${3:-debug}" start _ start="$(date +%s%3N)" - monkeydo "${BUILD_DIR}/debug/${device}.prg" "${device}" >"${app_log}" 2>&1 & + # Both monkeydo and the HARNESS line below append, so neither overwrites + # the other's output. + : >"${app_log}" + monkeydo "${BUILD_DIR}/${mode}/${device}.prg" "${device}" >>"${app_log}" 2>&1 & CIQ_MONKEYDO_PID=$! + local presses=0 glance_seen_at=0 tick=0 for _ in $(seq 1 $((FIRST_FRAME_TIMEOUT_S * 20))); do + tick=$((tick + 1)) + # The simulator starts apps that have a glance in the glance. Open the + # app from it, as a user would: 2 s after the glance appears (earlier + # presses get lost), again every 8 s if the app hasn't opened. + if ((glance_seen_at == 0)) && grep -q "DIAG shown glance" "${app_log}"; then + glance_seen_at=${tick} + fi + if ((glance_seen_at > 0 && presses < 3 && tick - glance_seen_at == 40 + presses * 160)); then + presses=$((presses + 1)) + python3 /work/tools/ui_flow.py --open-from-glance --device-dir "${CIQ_HOME}/Devices/${device}" \ + --window "$(ciq_simulator_window)" --out "${BUILD_DIR}/ui-input" >&2 || return 1 + fi if grep -q "DIAG first_frame" "${app_log}"; then echo "HARNESS first_frame wall_ms=$(($(date +%s%3N) - start))" >>"${app_log}" return 0 diff --git a/docker/scripts/screenshots.sh b/docker/scripts/screenshots.sh index 3fc52f5..414af52 100755 --- a/docker/scripts/screenshots.sh +++ b/docker/scripts/screenshots.sh @@ -1,23 +1,25 @@ #!/usr/bin/env bash # Author: cirakg & claude ai -# Summary: Screenshot the app on each device in the headless simulator and compare with the approved golden images. +# Summary: Run the scripted UI flow on each device in the headless simulator and compare every screen with its golden image. # # Usage: screenshots.sh DEVICE [DEVICE ...] (APPROVE=1 saves new goldens) -# Output: build/screenshots/.png (the watch screen only), plus a -# -diff.png when it differs from test/golden/.png. +# Output: build/screenshots//.png (the watch screen only), +# plus -diff.png where a screen differs from +# test/golden//.png. +# +# Uses the UI-test build (uitest.jungle): fake entries, PIN 123456 and a +# frozen clock, so every screenshot is reproducible. tools/ui_flow.py +# drives the app and checks the screens. set -euo pipefail # shellcheck source=ciq-env.sh source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh" -# Pause after the first frame, so animations and late redraws settle. -readonly SETTLE_S=1 - (($# > 0)) || die "usage: screenshots.sh DEVICE [DEVICE ...]" ciq_require_sdk ciq_require_key for device in "$@"; do - build-app.sh debug "${device}" + build-app.sh uitest "${device}" done ciq_start_display @@ -29,19 +31,18 @@ approve=() [[ "${APPROVE:-0}" == 1 ]] && approve=(--approve) failed=0 for device in "$@"; do - if ! ciq_launch_app "${device}" "${out}/${device}.log"; then + mkdir -p "${out}/${device}" + log_file="${out}/${device}/app.log" + if ! ciq_launch_app "${device}" "${log_file}" uitest; then ciq_stop_app - log "no first frame on ${device}; see build/screenshots/${device}.log" + log "no first frame on ${device}; see ${log_file}" failed=1 continue fi - sleep "${SETTLE_S}" - mkdir -p "${out}/raw" - import -window "$(ciq_simulator_window)" "${out}/raw/${device}-window.png" + python3 /work/tools/ui_flow.py --device-dir "${CIQ_HOME}/Devices/${device}" --log "${log_file}" \ + --window "$(ciq_simulator_window)" --out "${out}/${device}" --golden "/work/test/golden/${device}" \ + "${approve[@]}" || failed=1 ciq_stop_app - python3 /work/tools/sim_screenshot.py --raw "${out}/raw/${device}-window.png" \ - --device-dir "${CIQ_HOME}/Devices/${device}" --out "${out}/${device}.png" \ - --golden "/work/test/golden/${device}.png" "${approve[@]}" || failed=1 done ((failed == 0)) || die "screenshot checks failed; see build/screenshots/" -- cgit v1.2.3