aboutsummaryrefslogtreecommitdiff
path: root/docker/scripts
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-19 22:28:09 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-19 22:28:09 +0200
commit2373b6f02ef027a423762d5d6349cd9e0f36090e (patch)
tree7509b0b1905d7c593074bba8ed4c94e30e437604 /docker/scripts
parent35042ff04ffc50e55ff59a385849442be4429d6b (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'docker/scripts')
-rwxr-xr-xdocker/scripts/build-app.sh14
-rwxr-xr-xdocker/scripts/ciq-env.sh29
-rwxr-xr-xdocker/scripts/screenshots.sh31
3 files changed, 49 insertions, 25 deletions
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/<mode>/<DEVICE>.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/<DEVICE>.png (the watch screen only), plus a
-# <DEVICE>-diff.png when it differs from test/golden/<DEVICE>.png.
+# Output: build/screenshots/<DEVICE>/<screen>.png (the watch screen only),
+# plus <screen>-diff.png where a screen differs from
+# test/golden/<DEVICE>/<screen>.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/"