aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-25 23:51:52 +0100
committercirakg <cirakg@cirakg.xyz>2026-10-25 23:51:52 +0100
commitb8addc87f804789368612bf8e1be31a2bf28efe1 (patch)
treebece4051eb6f009eba05ffdd585420232741a7d7
parent140a6660f63f88d05ba9aae0bf1c4b401bb6574d (diff)
test(ui): second UI scenario, and the fixes it found
A second UI-test build (uitest-extra.jungle) covers the legacy PIN and the forced new PIN, Change and Reset PIN, a queued Require PIN, UPC-A, Code 39, the code error screen, the encoding progress, auto-lock on a barcode, the light theme and the glance without a code; the main flow adds the Steam and Library details and the menu's text screens. Fixed from what it found: Entry IDs tripped the watchdog with 110 entries; auto-lock didn't close a locked barcode; a queued Require PIN showed "Confirm in menu"; locked barcodes lacked the padlock; the PIN keypad's bottom row, the Reset PIN question, text-screen bottoms, the glance title and the light-theme warning colour. Goldens approved on all seven watches; only the Forerunner 70 was re-verified after the version bump. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rwxr-xr-xdocker/scripts/build-app.sh14
-rwxr-xr-xdocker/scripts/screenshots.sh34
-rw-r--r--source/app/AppInfo.mc2
-rw-r--r--source/app/Fixtures.mc56
-rw-r--r--source/settings/ChangeQueue.mc29
-rw-r--r--source/ui/CodeView.mc19
-rw-r--r--source/ui/DebugMenu.mc9
-rw-r--r--source/ui/GlanceView.mc23
-rw-r--r--source/ui/InfoView.mc4
-rw-r--r--source/ui/ListView.mc18
-rw-r--r--source/ui/MainMenu.mc113
-rw-r--r--source/ui/OtpView.mc4
-rw-r--r--source/ui/PinView.mc33
-rw-r--r--source/ui/Ui.mc8
-rw-r--r--source/util/TestMode.mc31
-rw-r--r--test/golden/fenix7/clock_status.pngbin0 -> 3490 bytes
-rw-r--r--test/golden/fenix7/code_39.pngbin0 -> 3265 bytes
-rw-r--r--test/golden/fenix7/code_error.pngbin0 -> 4410 bytes
-rw-r--r--test/golden/fenix7/code_qr_large.pngbin0 -> 4395 bytes
-rw-r--r--test/golden/fenix7/code_upca.pngbin0 -> 3344 bytes
-rw-r--r--test/golden/fenix7/entry_ids.pngbin0 -> 3634 bytes
-rw-r--r--test/golden/fenix7/entry_ids_next.pngbin0 -> 3832 bytes
-rw-r--r--test/golden/fenix7/glance.pngbin0 -> 2435 bytes
-rw-r--r--test/golden/fenix7/list_extra.pngbin0 -> 4895 bytes
-rw-r--r--test/golden/fenix7/list_locked.pngbin0 -> 4818 bytes
-rw-r--r--test/golden/fenix7/list_reset.pngbin0 -> 4797 bytes
-rw-r--r--test/golden/fenix7/lock_queued.pngbin0 -> 5420 bytes
-rw-r--r--test/golden/fenix7/menu_results.pngbin0 -> 2444 bytes
-rw-r--r--test/golden/fenix7/otp_library.pngbin0 -> 4316 bytes
-rw-r--r--test/golden/fenix7/otp_steam.pngbin0 -> 4315 bytes
-rw-r--r--test/golden/fenix7/pin.pngbin2918 -> 2771 bytes
-rw-r--r--test/golden/fenix7/pin_change.pngbin0 -> 3790 bytes
-rw-r--r--test/golden/fenix7/pin_create.pngbin0 -> 3790 bytes
-rw-r--r--test/golden/fenix7/pin_differ.pngbin0 -> 4417 bytes
-rw-r--r--test/golden/fenix7/pin_error.pngbin3292 -> 3145 bytes
-rw-r--r--test/golden/fenix7/pin_legacy.pngbin0 -> 3992 bytes
-rw-r--r--test/golden/fenix7/pin_legacy_typed.pngbin0 -> 3991 bytes
-rw-r--r--test/golden/fenix7/pin_new_forced.pngbin0 -> 4419 bytes
-rw-r--r--test/golden/fenix7/pin_repeat.pngbin0 -> 3953 bytes
-rw-r--r--test/golden/fenix7/pin_wait.pngbin3279 -> 3180 bytes
-rw-r--r--test/golden/fenix7/reset_confirm.pngbin0 -> 2590 bytes
-rw-r--r--test/golden/fr255s/clock_status.pngbin0 -> 2561 bytes
-rw-r--r--test/golden/fr255s/code_39.pngbin0 -> 2205 bytes
-rw-r--r--test/golden/fr255s/code_error.pngbin0 -> 3477 bytes
-rw-r--r--test/golden/fr255s/code_qr_large.pngbin0 -> 3067 bytes
-rw-r--r--test/golden/fr255s/code_upca.pngbin0 -> 2293 bytes
-rw-r--r--test/golden/fr255s/entry_ids.pngbin0 -> 2535 bytes
-rw-r--r--test/golden/fr255s/entry_ids_next.pngbin0 -> 2745 bytes
-rw-r--r--test/golden/fr255s/glance.pngbin0 -> 1812 bytes
-rw-r--r--test/golden/fr255s/list_extra.pngbin0 -> 3606 bytes
-rw-r--r--test/golden/fr255s/list_locked.pngbin0 -> 3531 bytes
-rw-r--r--test/golden/fr255s/list_reset.pngbin0 -> 3538 bytes
-rw-r--r--test/golden/fr255s/lock_queued.pngbin0 -> 3905 bytes
-rw-r--r--test/golden/fr255s/menu_results.pngbin0 -> 1837 bytes
-rw-r--r--test/golden/fr255s/otp_library.pngbin0 -> 3286 bytes
-rw-r--r--test/golden/fr255s/otp_steam.pngbin0 -> 3112 bytes
-rw-r--r--test/golden/fr255s/pin.pngbin1352 -> 1346 bytes
-rw-r--r--test/golden/fr255s/pin_change.pngbin0 -> 2193 bytes
-rw-r--r--test/golden/fr255s/pin_create.pngbin0 -> 2193 bytes
-rw-r--r--test/golden/fr255s/pin_differ.pngbin0 -> 2792 bytes
-rw-r--r--test/golden/fr255s/pin_error.pngbin1720 -> 1696 bytes
-rw-r--r--test/golden/fr255s/pin_legacy.pngbin0 -> 2231 bytes
-rw-r--r--test/golden/fr255s/pin_legacy_typed.pngbin0 -> 2249 bytes
-rw-r--r--test/golden/fr255s/pin_new_forced.pngbin0 -> 2809 bytes
-rw-r--r--test/golden/fr255s/pin_repeat.pngbin0 -> 2328 bytes
-rw-r--r--test/golden/fr255s/pin_wait.pngbin2058 -> 2083 bytes
-rw-r--r--test/golden/fr255s/reset_confirm.pngbin0 -> 1753 bytes
-rw-r--r--test/golden/fr70/about.pngbin15408 -> 15278 bytes
-rw-r--r--test/golden/fr70/clock_status.pngbin0 -> 13971 bytes
-rw-r--r--test/golden/fr70/code_39.pngbin0 -> 13364 bytes
-rw-r--r--test/golden/fr70/code_error.pngbin0 -> 19663 bytes
-rw-r--r--test/golden/fr70/code_qr_large.pngbin0 -> 12167 bytes
-rw-r--r--test/golden/fr70/code_upca.pngbin0 -> 14276 bytes
-rw-r--r--test/golden/fr70/entry_ids.pngbin0 -> 14105 bytes
-rw-r--r--test/golden/fr70/entry_ids_next.pngbin0 -> 13957 bytes
-rw-r--r--test/golden/fr70/glance.pngbin0 -> 28420 bytes
-rw-r--r--test/golden/fr70/list_extra.pngbin0 -> 24869 bytes
-rw-r--r--test/golden/fr70/list_locked.pngbin0 -> 21890 bytes
-rw-r--r--test/golden/fr70/list_reset.pngbin0 -> 22574 bytes
-rw-r--r--test/golden/fr70/lock_queued.pngbin0 -> 25102 bytes
-rw-r--r--test/golden/fr70/menu_results.pngbin0 -> 8002 bytes
-rw-r--r--test/golden/fr70/otp_library.pngbin0 -> 13339 bytes
-rw-r--r--test/golden/fr70/otp_steam.pngbin0 -> 14585 bytes
-rw-r--r--test/golden/fr70/pin.pngbin8869 -> 7862 bytes
-rw-r--r--test/golden/fr70/pin_change.pngbin0 -> 14948 bytes
-rw-r--r--test/golden/fr70/pin_create.pngbin0 -> 14948 bytes
-rw-r--r--test/golden/fr70/pin_differ.pngbin0 -> 17889 bytes
-rw-r--r--test/golden/fr70/pin_error.pngbin10833 -> 9863 bytes
-rw-r--r--test/golden/fr70/pin_legacy.pngbin0 -> 15963 bytes
-rw-r--r--test/golden/fr70/pin_legacy_typed.pngbin0 -> 15957 bytes
-rw-r--r--test/golden/fr70/pin_new_forced.pngbin0 -> 18272 bytes
-rw-r--r--test/golden/fr70/pin_repeat.pngbin0 -> 15604 bytes
-rw-r--r--test/golden/fr70/pin_wait.pngbin12737 -> 12003 bytes
-rw-r--r--test/golden/fr70/reset_confirm.pngbin0 -> 8206 bytes
-rw-r--r--test/golden/fr965/clock_status.pngbin0 -> 15417 bytes
-rw-r--r--test/golden/fr965/code_39.pngbin0 -> 11907 bytes
-rw-r--r--test/golden/fr965/code_error.pngbin0 -> 17179 bytes
-rw-r--r--test/golden/fr965/code_qr_large.pngbin0 -> 11541 bytes
-rw-r--r--test/golden/fr965/code_upca.pngbin0 -> 12635 bytes
-rw-r--r--test/golden/fr965/entry_ids.pngbin0 -> 15813 bytes
-rw-r--r--test/golden/fr965/entry_ids_next.pngbin0 -> 15245 bytes
-rw-r--r--test/golden/fr965/glance.pngbin0 -> 45280 bytes
-rw-r--r--test/golden/fr965/list_extra.pngbin0 -> 20693 bytes
-rw-r--r--test/golden/fr965/list_locked.pngbin0 -> 19317 bytes
-rw-r--r--test/golden/fr965/list_reset.pngbin0 -> 19421 bytes
-rw-r--r--test/golden/fr965/lock_queued.pngbin0 -> 19963 bytes
-rw-r--r--test/golden/fr965/menu_results.pngbin0 -> 10437 bytes
-rw-r--r--test/golden/fr965/otp_library.pngbin0 -> 20362 bytes
-rw-r--r--test/golden/fr965/otp_steam.pngbin0 -> 20483 bytes
-rw-r--r--test/golden/fr965/pin.pngbin11461 -> 10546 bytes
-rw-r--r--test/golden/fr965/pin_change.pngbin0 -> 13723 bytes
-rw-r--r--test/golden/fr965/pin_create.pngbin0 -> 13723 bytes
-rw-r--r--test/golden/fr965/pin_differ.pngbin0 -> 16030 bytes
-rw-r--r--test/golden/fr965/pin_error.pngbin13315 -> 12403 bytes
-rw-r--r--test/golden/fr965/pin_legacy.pngbin0 -> 14511 bytes
-rw-r--r--test/golden/fr965/pin_legacy_typed.pngbin0 -> 14386 bytes
-rw-r--r--test/golden/fr965/pin_new_forced.pngbin0 -> 16309 bytes
-rw-r--r--test/golden/fr965/pin_repeat.pngbin0 -> 14092 bytes
-rw-r--r--test/golden/fr965/pin_wait.pngbin15898 -> 15283 bytes
-rw-r--r--test/golden/fr965/reset_confirm.pngbin0 -> 10159 bytes
-rw-r--r--test/golden/instinct3amoled45mm/clock_status.pngbin0 -> 12637 bytes
-rw-r--r--test/golden/instinct3amoled45mm/code_39.pngbin0 -> 11489 bytes
-rw-r--r--test/golden/instinct3amoled45mm/code_error.pngbin0 -> 13363 bytes
-rw-r--r--test/golden/instinct3amoled45mm/code_qr_large.pngbin0 -> 12654 bytes
-rw-r--r--test/golden/instinct3amoled45mm/code_upca.pngbin0 -> 11690 bytes
-rw-r--r--test/golden/instinct3amoled45mm/entry_ids.pngbin0 -> 13600 bytes
-rw-r--r--test/golden/instinct3amoled45mm/entry_ids_next.pngbin0 -> 13687 bytes
-rw-r--r--test/golden/instinct3amoled45mm/glance.pngbin0 -> 31705 bytes
-rw-r--r--test/golden/instinct3amoled45mm/list_extra.pngbin0 -> 15546 bytes
-rw-r--r--test/golden/instinct3amoled45mm/list_locked.pngbin0 -> 14286 bytes
-rw-r--r--test/golden/instinct3amoled45mm/list_reset.pngbin0 -> 14380 bytes
-rw-r--r--test/golden/instinct3amoled45mm/lock_queued.pngbin0 -> 15383 bytes
-rw-r--r--test/golden/instinct3amoled45mm/menu_results.pngbin0 -> 10330 bytes
-rw-r--r--test/golden/instinct3amoled45mm/otp_library.pngbin0 -> 13367 bytes
-rw-r--r--test/golden/instinct3amoled45mm/otp_steam.pngbin0 -> 13493 bytes
-rw-r--r--test/golden/instinct3amoled45mm/pin.pngbin9569 -> 9675 bytes
-rw-r--r--test/golden/instinct3amoled45mm/pin_change.pngbin0 -> 10930 bytes
-rw-r--r--test/golden/instinct3amoled45mm/pin_create.pngbin0 -> 10930 bytes
-rw-r--r--test/golden/instinct3amoled45mm/pin_differ.pngbin0 -> 12090 bytes
-rw-r--r--test/golden/instinct3amoled45mm/pin_error.pngbin10508 -> 10472 bytes
-rw-r--r--test/golden/instinct3amoled45mm/pin_legacy.pngbin0 -> 10854 bytes
-rw-r--r--test/golden/instinct3amoled45mm/pin_legacy_typed.pngbin0 -> 10793 bytes
-rw-r--r--test/golden/instinct3amoled45mm/pin_new_forced.pngbin0 -> 12163 bytes
-rw-r--r--test/golden/instinct3amoled45mm/pin_repeat.pngbin0 -> 11245 bytes
-rw-r--r--test/golden/instinct3amoled45mm/pin_wait.pngbin11459 -> 11341 bytes
-rw-r--r--test/golden/instinct3amoled45mm/reset_confirm.pngbin0 -> 10406 bytes
-rw-r--r--test/golden/venusq2/about.pngbin0 -> 3988 bytes
-rw-r--r--test/golden/venusq2/clock_status.pngbin0 -> 3931 bytes
-rw-r--r--test/golden/venusq2/code_39.pngbin0 -> 2867 bytes
-rw-r--r--test/golden/venusq2/code_error.pngbin0 -> 4587 bytes
-rw-r--r--test/golden/venusq2/code_qr_large.pngbin0 -> 3894 bytes
-rw-r--r--test/golden/venusq2/code_upca.pngbin0 -> 2998 bytes
-rw-r--r--test/golden/venusq2/entry_ids.pngbin0 -> 3794 bytes
-rw-r--r--test/golden/venusq2/entry_ids_next.pngbin0 -> 3819 bytes
-rw-r--r--test/golden/venusq2/glance.pngbin0 -> 3352 bytes
-rw-r--r--test/golden/venusq2/import_results.pngbin4879 -> 4872 bytes
-rw-r--r--test/golden/venusq2/list_extra.pngbin0 -> 5162 bytes
-rw-r--r--test/golden/venusq2/list_locked.pngbin0 -> 4489 bytes
-rw-r--r--test/golden/venusq2/list_reset.pngbin0 -> 4462 bytes
-rw-r--r--test/golden/venusq2/lock_queued.pngbin0 -> 5489 bytes
-rw-r--r--test/golden/venusq2/menu_results.pngbin0 -> 4870 bytes
-rw-r--r--test/golden/venusq2/otp_library.pngbin0 -> 3352 bytes
-rw-r--r--test/golden/venusq2/otp_steam.pngbin0 -> 2956 bytes
-rw-r--r--test/golden/venusq2/pin.pngbin2734 -> 2505 bytes
-rw-r--r--test/golden/venusq2/pin_change.pngbin0 -> 3065 bytes
-rw-r--r--test/golden/venusq2/pin_create.pngbin0 -> 3065 bytes
-rw-r--r--test/golden/venusq2/pin_differ.pngbin0 -> 3709 bytes
-rw-r--r--test/golden/venusq2/pin_error.pngbin3152 -> 2930 bytes
-rw-r--r--test/golden/venusq2/pin_legacy.pngbin0 -> 3240 bytes
-rw-r--r--test/golden/venusq2/pin_legacy_typed.pngbin0 -> 3233 bytes
-rw-r--r--test/golden/venusq2/pin_new_forced.pngbin0 -> 3746 bytes
-rw-r--r--test/golden/venusq2/pin_repeat.pngbin0 -> 3185 bytes
-rw-r--r--test/golden/venusq2/pin_wait.pngbin3632 -> 3435 bytes
-rw-r--r--test/golden/venusq2/reset_confirm.pngbin0 -> 2466 bytes
-rw-r--r--test/golden/venux1/about.pngbin0 -> 17066 bytes
-rw-r--r--test/golden/venux1/clock_status.pngbin0 -> 14994 bytes
-rw-r--r--test/golden/venux1/code_39.pngbin0 -> 11350 bytes
-rw-r--r--test/golden/venux1/code_error.pngbin0 -> 16271 bytes
-rw-r--r--test/golden/venux1/code_qr_large.pngbin0 -> 10739 bytes
-rw-r--r--test/golden/venux1/code_upca.pngbin0 -> 11793 bytes
-rw-r--r--test/golden/venux1/entry_ids.pngbin0 -> 17459 bytes
-rw-r--r--test/golden/venux1/entry_ids_next.pngbin0 -> 18460 bytes
-rw-r--r--test/golden/venux1/glance.pngbin0 -> 43110 bytes
-rw-r--r--test/golden/venux1/import_results.pngbin26227 -> 23157 bytes
-rw-r--r--test/golden/venux1/list_extra.pngbin0 -> 23984 bytes
-rw-r--r--test/golden/venux1/list_locked.pngbin0 -> 21634 bytes
-rw-r--r--test/golden/venux1/list_reset.pngbin0 -> 24141 bytes
-rw-r--r--test/golden/venux1/lock_queued.pngbin0 -> 19387 bytes
-rw-r--r--test/golden/venux1/menu_results.pngbin0 -> 23155 bytes
-rw-r--r--test/golden/venux1/otp_library.pngbin0 -> 11906 bytes
-rw-r--r--test/golden/venux1/otp_steam.pngbin0 -> 12383 bytes
-rw-r--r--test/golden/venux1/pin.pngbin11861 -> 10939 bytes
-rw-r--r--test/golden/venux1/pin_change.pngbin0 -> 13043 bytes
-rw-r--r--test/golden/venux1/pin_create.pngbin0 -> 13043 bytes
-rw-r--r--test/golden/venux1/pin_differ.pngbin0 -> 15352 bytes
-rw-r--r--test/golden/venux1/pin_error.pngbin13551 -> 12636 bytes
-rw-r--r--test/golden/venux1/pin_legacy.pngbin0 -> 13887 bytes
-rw-r--r--test/golden/venux1/pin_legacy_typed.pngbin0 -> 13736 bytes
-rw-r--r--test/golden/venux1/pin_new_forced.pngbin0 -> 15600 bytes
-rw-r--r--test/golden/venux1/pin_repeat.pngbin0 -> 13523 bytes
-rw-r--r--test/golden/venux1/pin_wait.pngbin15922 -> 15184 bytes
-rw-r--r--test/golden/venux1/reset_confirm.pngbin0 -> 10484 bytes
-rw-r--r--tools/gen_fixture_wrap.py15
-rw-r--r--tools/tests/test_gen_fixture_wrap.py8
-rw-r--r--tools/tests/test_ui_flow.py10
-rw-r--r--tools/ui_flow.py417
-rw-r--r--uitest-extra.jungle8
-rw-r--r--uitest.jungle6
208 files changed, 772 insertions, 89 deletions
diff --git a/docker/scripts/build-app.sh b/docker/scripts/build-app.sh
index 5ead272..4d027a1 100755
--- a/docker/scripts/build-app.sh
+++ b/docker/scripts/build-app.sh
@@ -1,13 +1,14 @@
#!/usr/bin/env bash
# Author: cirakg & claude ai
-# Summary: Compile the watch app for one device: debug, release, unit-test, UI-test or watchdog stress build.
+# Summary: Compile the watch app for one device: debug, release, unit-test, UI-test (two scenarios) or watchdog stress build.
#
-# Usage: build-app.sh debug|release|test|uitest|stress DEVICE
+# Usage: build-app.sh debug|release|test|uitest|uitest-extra|stress 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. UI-test builds add uitest.jungle (fixed clock,
-# fake entries; see source/util/TestMode.mc), stress builds stress.jungle (the
+# fake entries; see source/util/TestMode.mc), or uitest-extra.jungle for the
+# extra scenario's fixture, stress builds stress.jungle (the
# watchdog stress test, source/util/StressTest.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.
@@ -15,8 +16,8 @@ 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|uitest|stress DEVICE}"
-device="${2:?usage: build-app.sh debug|release|test|uitest|stress DEVICE}"
+mode="${1:?usage: build-app.sh debug|release|test|uitest|uitest-extra|stress DEVICE}"
+device="${2:?usage: build-app.sh debug|release|test|uitest|uitest-extra|stress DEVICE}"
ciq_require_sdk
ciq_require_key
@@ -30,12 +31,13 @@ case "${mode}" in
debug) ;;
release) args+=(-r) ;;
uitest) jungles="${jungles};/work/uitest.jungle" ;;
+ uitest-extra) jungles="${jungles};/work/uitest-extra.jungle" ;;
stress) jungles="${jungles};/work/stress.jungle" ;;
test)
jungles="${jungles};/work/test.jungle"
args+=(-t)
;;
- *) die "unknown build mode '${mode}' (use debug, release, test, uitest or stress)" ;;
+ *) die "unknown build mode '${mode}' (use debug, release, test, uitest, uitest-extra or stress)" ;;
esac
out_dir="${BUILD_DIR}/${mode}"
diff --git a/docker/scripts/screenshots.sh b/docker/scripts/screenshots.sh
index c0d91d6..6c3161f 100755
--- a/docker/scripts/screenshots.sh
+++ b/docker/scripts/screenshots.sh
@@ -11,7 +11,10 @@
# frozen clock, so every screenshot is reproducible. tools/ui_flow.py
# drives the app and checks the screens. Then the app is launched again and
# its glance, which now shows the favourite's code (the first run sealed it),
-# is checked as glance_code.png.
+# is checked as glance_code.png. Then the second UI-test build
+# (uitest-extra.jungle: legacy PIN, queued "Require PIN", more symbologies)
+# runs the extra scenario (ui_flow.py --scenario extra), and its glance,
+# without a code, is checked as glance.png.
set -euo pipefail
# shellcheck source=ciq-env.sh
source "$(dirname "${BASH_SOURCE[0]}")/ciq-env.sh"
@@ -22,6 +25,7 @@ ciq_require_key
for device in "$@"; do
build-app.sh uitest "${device}"
+ build-app.sh uitest-extra "${device}"
done
ciq_start_display
@@ -29,15 +33,16 @@ ciq_start_simulator
out="${BUILD_DIR}/screenshots"
-# A new launch starts in the glance; screenshot it before anything opens the app.
+# A new launch of MODE's build starts in the glance; screenshot it as SCREEN
+# (its "DIAG shown" name) before anything opens the app.
shoot_glance() {
- local device="$1" glance_log="${out}/$1/glance.log"
+ local device="$1" mode="$2" screen="$3" glance_log="${out}/$1/${3}.log"
: >"${glance_log}"
- monkeydo "${BUILD_DIR}/uitest/${device}.prg" "${device}" >>"${glance_log}" 2>&1 &
+ monkeydo "${BUILD_DIR}/${mode}/${device}.prg" "${device}" >>"${glance_log}" 2>&1 &
CIQ_MONKEYDO_PID=$!
local shown=0
for _ in $(seq 1 600); do
- if grep -q "DIAG shown glance_code" "${glance_log}"; then
+ if grep -Eq "DIAG shown ${screen}[[:space:]]*$" "${glance_log}"; then
shown=1
break
fi
@@ -45,12 +50,12 @@ shoot_glance() {
done
if ((shown == 0)); then
ciq_stop_app
- log "${device}: the glance with the favourite's code didn't appear; see ${glance_log}"
+ log "${device}: the glance (${screen}) didn't appear; see ${glance_log}"
return 1
fi
sleep 1
local status=0
- python3 /work/tools/ui_flow.py --shot glance_code --device-dir "${CIQ_HOME}/Devices/${device}" \
+ python3 /work/tools/ui_flow.py --shot "${screen}" --device-dir "${CIQ_HOME}/Devices/${device}" \
--window "$(ciq_simulator_window)" --out "${out}/${device}" --golden "/work/test/golden/${device}" \
"${approve[@]}" || status=1
ciq_stop_app
@@ -73,7 +78,20 @@ for device in "$@"; do
--window "$(ciq_simulator_window)" --out "${out}/${device}" --golden "/work/test/golden/${device}" \
"${approve[@]}" || failed=1
ciq_stop_app
- shoot_glance "${device}" || failed=1
+ shoot_glance "${device}" uitest glance_code || failed=1
+ # The extra scenario: its own build and launch (README 17.4).
+ extra_log="${out}/${device}/app-extra.log"
+ if ! ciq_launch_app "${device}" "${extra_log}" uitest-extra; then
+ ciq_stop_app
+ log "no first frame of the extra scenario on ${device}; see ${extra_log}"
+ failed=1
+ continue
+ fi
+ python3 /work/tools/ui_flow.py --scenario extra --device-dir "${CIQ_HOME}/Devices/${device}" --log "${extra_log}" \
+ --window "$(ciq_simulator_window)" --out "${out}/${device}" --golden "/work/test/golden/${device}" \
+ "${approve[@]}" || failed=1
+ ciq_stop_app
+ shoot_glance "${device}" uitest-extra glance || failed=1
done
((failed == 0)) || die "screenshot checks failed; see build/screenshots/"
diff --git a/source/app/AppInfo.mc b/source/app/AppInfo.mc
index 163486c..1e6fe3c 100644
--- a/source/app/AppInfo.mc
+++ b/source/app/AppInfo.mc
@@ -12,5 +12,5 @@ import Toybox.Lang;
module AppInfo {
// Semantic version of the app: MAJOR.MINOR.PATCH.
- const VERSION = "0.8.0";
+ const VERSION = "0.9.0";
}
diff --git a/source/app/Fixtures.mc b/source/app/Fixtures.mc
index 01b15a8..08a51a1 100644
--- a/source/app/Fixtures.mc
+++ b/source/app/Fixtures.mc
@@ -12,6 +12,11 @@
// result, with the 4-digit PIN marker (KeyManager.PIN_LENGTH_KEY).
// Unlocking it with the app's own code also cross-checks the wrap format
// against the Python implementation (test/storage/PinTest.mc).
+//
+// The extra UI-test scenario (TestMode.SCENARIO_EXTRA, uitest-extra.jungle)
+// loads loadExtra() instead: FIXTURE_LEGACY_WRAP, the same master key under
+// the 6-digit PIN FIXTURE_LEGACY_PIN without the marker (a PIN from before
+// the 4-digit rule), and the entries and forms its flow needs.
import Toybox.Application;
import Toybox.Lang;
@@ -22,6 +27,8 @@ module Fixtures {
const FIXTURE_PIN = "1234";
const FIXTURE_WRAP = "101112131415161718191a1b1c1d1e1f000007d001202122232425262728292a2b2c2d2e2f133549536635b7a7ae3add45a59c42953d4c666538390a99305164dba9c99961e7148e1c561b6cfc70cfde7f9e3e59179464ce65bf91dd08807ff8a52027d77c90c2b692ca22fe4cbd3fb20ffb440137"; // lint: allow-literal (fake fixture)
const FIXTURE_MASTER_KEY = "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"; // lint: allow-literal (fake fixture)
+ const FIXTURE_LEGACY_PIN = "123456";
+ const FIXTURE_LEGACY_WRAP = "101112131415161718191a1b1c1d1e1f000007d001202122232425262728292a2b2c2d2e2f857b961d4854e06740f602627bfb952efaac5383e800768221bf3a7b292ee881e2ce2f76b9dd744b2ac32322473df351929bc6dfcbfc2941527ae8b90614a8a6a6bd0f72d33161bafd16b1039caa670c"; // lint: allow-literal (fake fixture)
// Erase all app storage (UI-test build only).
function resetStore() as Void {
@@ -32,6 +39,18 @@ module Fixtures {
// so one entry can be locked; otherwise locked demo entries go to the
// pending queue until a PIN exists.
function load(vault as Vault) as Void {
+ var extra = TestMode.ON && TestMode.SCENARIO == TestMode.SCENARIO_EXTRA;
+ if (TestMode.ON) {
+ // The extra scenario's screens use the light theme, the main
+ // one's the dark theme. Properties outlive a launch in the
+ // simulator, so both set it.
+ Application.Properties.setValue("optTheme", extra ? Options.THEME_LIGHT : Options.THEME_DARK);
+ AppState.options.load();
+ }
+ if (extra) {
+ loadExtra(vault);
+ return;
+ }
if (TestMode.ON && !vault.hasPin()) {
Application.Storage.setValue(KeyManager.WRAP_KEY, Bytes.fromHex(FIXTURE_WRAP));
Application.Storage.setValue(KeyManager.PIN_LENGTH_KEY, KeyManager.PIN_DIGITS);
@@ -75,6 +94,43 @@ module Fixtures {
}
}
+ // The extra UI-test scenario (README 17.4; tools/ui_flow.py
+ // run_extra_flow): the legacy PIN, a locked TOTP, the symbologies the
+ // main fixture lacks (UPC-A, Code 39), an EAN-13 whose check digit is
+ // wrong (the code error screen) and a QR large enough that its encoding
+ // progress shows for a while. The Manage form asks for "Require PIN" on
+ // the UPC-A card while the watch is locked, so it waits for the next
+ // PIN; the glance option is off, so the glance shows the entry count.
+ // Its screens use the light theme (set in load()).
+ function loadExtra(vault as Vault) as Void {
+ Application.Storage.setValue(KeyManager.WRAP_KEY, Bytes.fromHex(FIXTURE_LEGACY_WRAP));
+ vault.session.unlock(Bytes.fromHex(FIXTURE_MASTER_KEY));
+ var rfcKey = "12345678901234567890"; // lint: allow-literal (RFC 4226 test key)
+ vault.add(otp("GitHub", "alice@example.com", "Work", Entry.OTP_TOTP, Hashes.SHA1, 6, 30, rfcKey, false));
+ vault.add(otp("Bank", "carol", "Finance", Entry.OTP_TOTP, Hashes.SHA512, 6, 30,
+ "1234567890123456789012345678901234567890123456789012345678901234", true)); // lint: allow-literal (RFC 6238 test key)
+ vault.add(code("Shop card", Codes.UPCA, "036000291452")); // fake; a valid UPC-A check digit
+ vault.add(code("Badge", Codes.CODE39, "FAKE-39"));
+ vault.add(code("Broken card", Codes.EAN13, "5901234123458")); // the check digit should be 7
+ // Fake text of 199 characters: a version 10 QR in byte mode, which
+ // still fits the smallest screen at 2 px per module.
+ var text = "Fake event ticket 0042 for the UI test.";
+ while (text.length() < 160) {
+ text += " Row 12, seat 34, gate b, valid all day.";
+ }
+ vault.add(code("Big QR", Codes.QR, text));
+ vault.lock();
+ Application.Properties.setValue("optGlanceCode", false);
+ vault.glanceCode = false;
+ Application.Properties.setValue("impApply", false);
+ Application.Properties.setValue("otpAdd", false);
+ Application.Properties.setValue("codeAdd", false);
+ var ids = vault.ids();
+ Application.Properties.setValue("mgId", ids[2]); // Shop card
+ Application.Properties.setValue("mgAction", ManageAction.LOCK);
+ Application.Properties.setValue("mgApply", true);
+ }
+
// UI-test build: the glance option, and the other forms, filled in as
// the phone would (README 16, M7 end-to-end): Add OTP (a TOTP whose code the screenshot shows),
// Add code (a Code 128 card whose screenshot must decode) and Manage
diff --git a/source/settings/ChangeQueue.mc b/source/settings/ChangeQueue.mc
index c5919f6..5de74e0 100644
--- a/source/settings/ChangeQueue.mc
+++ b/source/settings/ChangeQueue.mc
@@ -27,6 +27,10 @@ module ChangeQueue {
// all" that follows it (ChangeQueueJob). Replaced at every PIN.
var _lockLines as Array<String> = [] as Array<String>;
+ // countToConfirm()'s cached result and the vault it is for.
+ var _toConfirm as Number? = null;
+ var _toConfirmFor as Vault? = null;
+
function count(vault as Vault) as Number {
var stored = vault.storage().getValue(KEY);
return stored instanceof Array ? stored.size() : 0;
@@ -42,6 +46,7 @@ module ChangeQueue {
var plain = action.toBytes();
items.add(SealedBox.seal(vault.keyManager.deviceKeys(), Bytes.fromUtf8(AAD), plain));
Bytes.wipe(plain);
+ changed();
try {
vault.storage().setValue(KEY, items);
} catch (e instanceof Lang.Exception) {
@@ -84,6 +89,26 @@ module ChangeQueue {
return out;
}
+ // The number of waiting changes that need "Confirm all": all but
+ // "Require PIN", which applies at the next PIN by itself. For the list's
+ // title and the menu; cached until the queue changes, because the list
+ // asks at every draw and counting opens every item.
+ function countToConfirm(vault as Vault) as Number {
+ var n = _toConfirm;
+ if (n == null || _toConfirmFor != vault) {
+ var total = count(vault);
+ n = total > 0 ? total - countLocks(vault) : 0;
+ _toConfirm = n;
+ _toConfirmFor = vault;
+ }
+ return n;
+ }
+
+ // The queue changed: count again next time.
+ function changed() as Void {
+ _toConfirm = null;
+ }
+
// The number of waiting "Require PIN" actions (applied at the next PIN).
function countLocks(vault as Vault) as Number {
var actions = load(vault);
@@ -104,6 +129,7 @@ module ChangeQueue {
// already locked) is dropped too.
function applyLocks(vault as Vault) as Void {
_lockLines = [] as Array<String>;
+ changed();
var stored = vault.storage().getValue(KEY);
if (!(stored instanceof Array) || !vault.session.isUnlocked()) {
return;
@@ -127,6 +153,7 @@ module ChangeQueue {
kept.add(box as Application.Storage.ValueType); // damaged items are dropped by the next clear()
}
}
+ Diag.data("locks_applied", "count=" + _lockLines.size()); // for UI scripts: no names
if (kept.size() == stored.size()) {
return;
}
@@ -150,12 +177,14 @@ module ChangeQueue {
}
function clear(vault as Vault) as Void {
+ changed();
vault.storage().deleteValue(KEY);
}
// Remove the first n items (the ones just applied), keeping any that
// arrived from the phone meanwhile.
function drop(vault as Vault, n as Number) as Void {
+ changed();
var stored = vault.storage().getValue(KEY);
if (!(stored instanceof Array) || stored.size() <= n) {
clear(vault);
diff --git a/source/ui/CodeView.mc b/source/ui/CodeView.mc
index efa23d2..03f1fa7 100644
--- a/source/ui/CodeView.mc
+++ b/source/ui/CodeView.mc
@@ -27,6 +27,9 @@
// ticker timer does the renewing (and redraws the progress while
// encoding), so a code screen never needs more timers.
// * Too big: a message with the size needed.
+// * A locked code closes back to the list when auto-lock fires while it
+// is open (README 7.5), like a locked OTP entry: the ticker keeps
+// running for it and checks the session.
// The payload is decrypted when the screen opens and wiped once encoded.
import Toybox.Attention;
@@ -70,6 +73,9 @@ class CodeView extends WatchUi.View {
_shownLogged = false;
encode();
boostBrightness();
+ if (header.locked) {
+ startTicker();
+ }
}
function onHide() as Void {
@@ -143,8 +149,13 @@ class CodeView extends WatchUi.View {
}
// Progress redraws while a job runs; brightness renewals during the
- // minute. The ticker stops when neither is needed.
+ // minute; auto-lock for a locked code. The ticker stops when none of
+ // them is needed.
function onTick() as Void {
+ if (header.locked && AppState.checkAutoLock()) {
+ WatchUi.popView(WatchUi.SLIDE_RIGHT);
+ return;
+ }
if (_job != null) {
WatchUi.requestUpdate();
}
@@ -157,7 +168,7 @@ class CodeView extends WatchUi.View {
light();
}
}
- if (_job == null && _boostStart < 0) {
+ if (_job == null && _boostStart < 0 && !header.locked) {
stopTicker();
}
}
@@ -266,8 +277,10 @@ class CodeView extends WatchUi.View {
} else if (code instanceof RunsPainter) {
drawMatrix(dc, code, smallFont, smallH);
} else if (job != null) {
+ var percent = job.percent();
dc.drawText(w / 2, h / 2 - smallH / 2, smallFont, (job instanceof RunsPainter ? "Drawing… " : "Encoding… ")
- + job.percent() + "%", Graphics.TEXT_JUSTIFY_CENTER);
+ + percent + "%", Graphics.TEXT_JUSTIFY_CENTER);
+ Diag.data("code_progress", "percent=" + percent); // for UI scripts
} else {
drawMessage(dc, code instanceof Codes.CodeError ? code.message : "…", smallFont, smallH);
}
diff --git a/source/ui/DebugMenu.mc b/source/ui/DebugMenu.mc
index f1ab407..452cb1d 100644
--- a/source/ui/DebugMenu.mc
+++ b/source/ui/DebugMenu.mc
@@ -40,6 +40,8 @@ module DebugMenu {
}
}
+ const BENCH_HEADERS = 20;
+
// The running benchmark (kept so its timer isn't collected).
var bench as BenchRunner? = null;
@@ -74,12 +76,15 @@ module DebugMenu {
} else if (_step == 2) {
_lines.add("PIN derivation: " + Kdf.calibrate(1000, Kdf.ITERATIONS_PER_STEP) + " iterations per second");
} else if (_step == 3) {
+ // At most BENCH_HEADERS: all 110 of the UI test's entries in
+ // one event tripped the watchdog.
var vault = AppState.theVault();
var ids = vault.ids();
- for (var i = 0; i < ids.size(); i++) {
+ var n = ids.size() < BENCH_HEADERS ? ids.size() : BENCH_HEADERS;
+ for (var i = 0; i < n; i++) {
vault.header(ids[i]);
}
- _lines.add("Headers: " + ids.size() + " in " + (System.getTimer() - t) + " ms");
+ _lines.add("Headers: " + n + " in " + (System.getTimer() - t) + " ms");
} else if (_step == 4) {
SelfTest.run();
_lines.add("Self-test: " + (System.getTimer() - t) + " ms");
diff --git a/source/ui/GlanceView.mc b/source/ui/GlanceView.mc
index 4540f2a..6be18f3 100644
--- a/source/ui/GlanceView.mc
+++ b/source/ui/GlanceView.mc
@@ -89,8 +89,15 @@ class OtpGlanceView extends WatchUi.GlanceView {
var h = dc.getHeight();
var top = h / 2 - dc.getFontHeight(Graphics.FONT_GLANCE);
var x = inset(dc, top);
+ // In capitals the name reaches the round screen's edge on the
+ // Forerunner 70 and 965, which cuts the last letter; the narrower
+ // mixed case fits there.
+ var title = "OTP WALLET";
+ if (x + dc.getTextWidthInPixels(title, Graphics.FONT_GLANCE) > dc.getWidth() - edgeCut(top)) {
+ title = "OTP Wallet";
+ }
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
- dc.drawText(x, top, Graphics.FONT_GLANCE, "OTP WALLET", Graphics.TEXT_JUSTIFY_LEFT);
+ dc.drawText(x, top, Graphics.FONT_GLANCE, title, Graphics.TEXT_JUSTIFY_LEFT);
dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT);
dc.drawText(x, h / 2, Graphics.FONT_GLANCE, count == 1 ? "1 entry" : count + " entries",
Graphics.TEXT_JUSTIFY_LEFT);
@@ -103,15 +110,23 @@ class OtpGlanceView extends WatchUi.GlanceView {
// screen's top-left curve; on most round watches this is 0 or a few
// pixels).
private function inset(dc as Graphics.Dc, y as Number) as Number {
+ var left = (System.getDeviceSettings().screenWidth - dc.getWidth()) / 2;
+ var edge = edgeCut(y);
+ return edge > left ? edge - left : 0;
+ }
+
+ // How far a round screen's edge reaches in from each side at height y
+ // (0 on other shapes), with the same assumption: the glance starts at
+ // the top of the screen. The right end of a glance reaches the screen's
+ // edge, so this is also what the curve cuts off there.
+ private function edgeCut(y as Number) as Number {
var screen = System.getDeviceSettings();
if (screen.screenShape != System.SCREEN_SHAPE_ROUND) {
return 0;
}
var r = screen.screenWidth / 2;
var dy = r - y;
- var edge = r - Math.sqrt(r * r - dy * dy).toNumber();
- var left = (screen.screenWidth - dc.getWidth()) / 2;
- return edge > left ? edge - left : 0;
+ return r - Math.sqrt(r * r - dy * dy).toNumber();
}
// The label, the code and a bar for the seconds left in the period.
diff --git a/source/ui/InfoView.mc b/source/ui/InfoView.mc
index a965713..877ca51 100644
--- a/source/ui/InfoView.mc
+++ b/source/ui/InfoView.mc
@@ -65,7 +65,9 @@ class InfoView extends WatchUi.View {
dc.drawText(w / 2, y, Graphics.FONT_SMALL, Ui.fit(dc, _title, Graphics.FONT_SMALL, 2 * Ui.halfWidthAt(dc, y)),
Graphics.TEXT_JUSTIFY_CENTER);
y += dc.getFontHeight(Graphics.FONT_SMALL) + 4;
- var bottom = Ui.isRound() ? h - h / 10 : h - 4;
+ // The same margin on rectangles: the Venu X1's rounded corners cut
+ // off a last line drawn at the very bottom, and the scroll arrow.
+ var bottom = h - h / 10;
var lines = _lines;
if (lines == null) {
// Wrap to the narrowest width the text area has.
diff --git a/source/ui/ListView.mc b/source/ui/ListView.mc
index de73cce..36df798 100644
--- a/source/ui/ListView.mc
+++ b/source/ui/ListView.mc
@@ -3,7 +3,8 @@
//
// Layout (README 13.1, 13.3): a title, then rows of two lines each (name;
// group and account) with the code and a countdown on the right for unlocked
-// OTP entries, a padlock for locked entries and a barcode icon for codes.
+// OTP entries, a padlock for locked entries (codes too: opening them asks
+// for the PIN) and a barcode icon for other codes.
// The focused row is outlined; the list scrolls to keep it visible and
// centres short lists. On round screens each row is as wide as the circle
// allows at its height.
@@ -186,7 +187,8 @@ class ListView extends WatchUi.View {
// "Importing… 40%" while an import runs, "Results ready" when settings
// results haven't been looked at, "Confirm in menu" while phone changes
- // wait for the PIN, else the app name.
+ // wait to be confirmed with the PIN (a waiting "Require PIN" applies at
+ // the next PIN by itself), else the app name.
private function title() as String {
var p = AppState.processor;
var busy = p != null ? p.busyText() : null;
@@ -197,7 +199,7 @@ class ListView extends WatchUi.View {
return "Results ready (menu)";
}
var v = AppState.vault;
- return v != null && ChangeQueue.count(v) > 0 ? "Confirm in menu" : "OTP Wallet";
+ return v != null && ChangeQueue.countToConfirm(v) > 0 ? "Confirm in menu" : "OTP Wallet";
}
private function drawEmpty(dc as Graphics.Dc, top as Number, bottom as Number) as Void {
@@ -283,7 +285,7 @@ class ListView extends WatchUi.View {
var right = x1 - 8;
if (header == null) {
- dc.setColor(Ui.WARN_COLOR, Graphics.COLOR_TRANSPARENT);
+ dc.setColor(Ui.warn(), Graphics.COLOR_TRANSPARENT);
dc.drawText(left, lineY, nameFont, Ui.fit(dc, "Damaged " + id, nameFont, right - left), Graphics.TEXT_JUSTIFY_LEFT);
return true;
}
@@ -297,12 +299,12 @@ class ListView extends WatchUi.View {
var secondRight = right;
var complete = true;
var iconSize = nameH * 2 / 3;
- if (header.kind == Entry.KIND_CODE) {
- Ui.drawBarcode(dc, right - iconSize, lineY + (nameH - iconSize) / 2, iconSize, Ui.dim());
- nameRight -= iconSize + 6;
- } else if (header.locked) {
+ if (header.locked) {
Ui.drawLock(dc, right - iconSize, lineY + (nameH - iconSize) / 2, iconSize, Ui.dim());
nameRight -= iconSize + 6;
+ } else if (header.kind == Entry.KIND_CODE) {
+ Ui.drawBarcode(dc, right - iconSize, lineY + (nameH - iconSize) / 2, iconSize, Ui.dim());
+ nameRight -= iconSize + 6;
} else {
var now = AppState.now();
var text = AppState.cryptoReady ? AppState.theCodes().cached(header.id, now) : null;
diff --git a/source/ui/MainMenu.mc b/source/ui/MainMenu.mc
index 8863c6d..db59f4a 100644
--- a/source/ui/MainMenu.mc
+++ b/source/ui/MainMenu.mc
@@ -18,7 +18,10 @@ module MainMenu {
var menu = new WatchUi.Menu2({:title => "OTP Wallet"});
var waiting = ChangeQueue.count(vault);
if (waiting > 0) {
- menu.addItem(new WatchUi.MenuItem("Phone changes", waiting + " to confirm", :queue, null));
+ // Only "Require PIN" waiting: it applies at the next PIN anyway.
+ var toConfirm = ChangeQueue.countToConfirm(vault);
+ menu.addItem(new WatchUi.MenuItem("Phone changes", toConfirm > 0 ? toConfirm + " to confirm" : "Apply at next PIN",
+ :queue, null));
}
if (vault.session.isUnlocked()) {
menu.addItem(new WatchUi.MenuItem("Lock now", null, :lock, null));
@@ -32,28 +35,29 @@ module MainMenu {
menu.addItem(new WatchUi.MenuItem("Clock status", null, :clock, null));
menu.addItem(new WatchUi.MenuItem("About", null, :about, null));
DebugMenu.addItems(menu);
+ logItems(menu);
return menu;
}
- function entryIdLines() as Array<String> {
- var vault = AppState.theVault();
- var ids = vault.ids();
- var lines = [] as Array<String>;
- for (var i = 0; i < ids.size(); i++) {
- var header = vault.header(ids[i]);
- var name = header == null ? "damaged" : (header.name.length() > 0 ? header.name : "Locked entry");
- lines.add(ids[i] + " " + name);
- }
- if (lines.size() == 0) {
- lines.add("No entries yet.");
- }
- var pending = vault.pendingIds().size();
- if (pending > 0) {
- lines.add(pending + " locked entries wait for your PIN.");
+ // Debug builds: the item labels in order, so UI scripts can find an
+ // item on touch-only watches, whose menus have no focus to move.
+ (:debug)
+ function logItems(menu as WatchUi.Menu2) as Void {
+ var labels = "";
+ for (var i = 0; menu.getItem(i) != null; i++) {
+ labels += (i > 0 ? "|" : "") + (menu.getItem(i) as WatchUi.MenuItem).getLabel();
}
- return lines;
+ Diag.data("menu", "items=" + labels);
+ }
+
+ (:release)
+ function logItems(menu as WatchUi.Menu2) as Void {
}
+ // The Entry IDs screen: "Reading…" while EntryIdsJob reads the
+ // headers on a timer, then the list (kept so the runner isn't collected).
+ var idsScreen as EntryIdsScreen? = null;
+
function clockLines() as Array<String> {
var lines = [] as Array<String>;
var watch = Clock.nowUnix(0);
@@ -118,7 +122,9 @@ class MainMenuDelegate extends WatchUi.Menu2InputDelegate {
WatchUi.switchToView(new PinView(PinView.MODE_UNLOCK, then.method(:run)), new PinDelegate(), WatchUi.SLIDE_LEFT);
}
} else if (id == :reset) {
- WatchUi.pushView(new WatchUi.Confirmation("Erase locked entries and set a new PIN?"), new ResetPinConfirmation(),
+ // Two short lines (the item said "Reset PIN"): the native dialog shows
+ // two lines and cuts longer text mid-word.
+ WatchUi.pushView(new WatchUi.Confirmation("Erase locked\nentries?"), new ResetPinConfirmation(),
WatchUi.SLIDE_LEFT);
} else if (id == :results) {
WatchUi.switchToView(new InfoView("Results", Results.lines(), "results"), new InfoDelegate(),
@@ -127,7 +133,9 @@ class MainMenuDelegate extends WatchUi.Menu2InputDelegate {
} else if (id == :queue) {
WatchUi.switchToView(QueueMenu.build(), new QueueMenuDelegate(), WatchUi.SLIDE_LEFT);
} else if (id == :ids) {
- WatchUi.switchToView(new InfoView("Entry IDs", MainMenu.entryIdLines(), "ids"), new InfoDelegate(), WatchUi.SLIDE_LEFT);
+ var screen = new EntryIdsScreen();
+ MainMenu.idsScreen = screen;
+ screen.start();
} else if (id == :clock) {
WatchUi.switchToView(new InfoView("Clock status", MainMenu.clockLines(), "clock"), new InfoDelegate(), WatchUi.SLIDE_LEFT);
} else if (id == :about) {
@@ -138,6 +146,73 @@ class MainMenuDelegate extends WatchUi.Menu2InputDelegate {
}
}
+// Reads one header per entry, a few per timer step: reading 110 in one
+// event tripped the watchdog. Ends with the lines of the Entry IDs screen.
+class EntryIdsJob extends Jobs.Job {
+
+ static const HEADERS_PER_STEP = 10;
+
+ var lines as Array<String> = [] as Array<String>;
+ private var _ids as Array<String>;
+ private var _at as Number = 0;
+
+ function initialize() {
+ Job.initialize();
+ _ids = AppState.theVault().ids();
+ }
+
+ function step(budget as Number) as Boolean {
+ var vault = AppState.theVault();
+ var stop = _at + HEADERS_PER_STEP * budget;
+ stop = stop < _ids.size() ? stop : _ids.size();
+ for (; _at < stop; _at++) {
+ var id = _ids[_at];
+ var header = vault.header(id);
+ var name = header == null ? "damaged" : (header.name.length() > 0 ? header.name : "Locked entry");
+ lines.add(id + " " + name);
+ }
+ if (_at < _ids.size()) {
+ return false;
+ }
+ if (lines.size() == 0) {
+ lines.add("No entries yet.");
+ }
+ var pending = vault.pendingIds().size();
+ if (pending > 0) {
+ lines.add(pending + " locked entries wait for your PIN.");
+ }
+ return true;
+ }
+}
+
+// Shows "Reading…" (replacing the menu), runs EntryIdsJob, then replaces
+// that screen with the IDs, unless the user left it meanwhile.
+class EntryIdsScreen {
+
+ private var _job as EntryIdsJob = new EntryIdsJob();
+ // The timer driver (kept so it isn't collected).
+ var runner as Jobs.JobRunner? = null;
+ private var _reading as InfoView = new InfoView("Entry IDs", ["Reading…"] as Array<String>, "ids_reading");
+
+ function initialize() {
+ }
+
+ function start() as Void {
+ WatchUi.switchToView(_reading, new InfoDelegate(), WatchUi.SLIDE_LEFT);
+ var r = new Jobs.JobRunner(_job, 1, method(:done));
+ runner = r;
+ r.start();
+ }
+
+ function done() as Void {
+ runner = null;
+ MainMenu.idsScreen = null;
+ if (WatchUi.getCurrentView()[0] == _reading) {
+ WatchUi.switchToView(new InfoView("Entry IDs", _job.lines, "ids"), new InfoDelegate(), WatchUi.SLIDE_IMMEDIATE);
+ }
+ }
+}
+
// After unlocking from the menu's "Change PIN": ask for the new PIN.
class ChangePinAfterUnlock {
diff --git a/source/ui/OtpView.mc b/source/ui/OtpView.mc
index 1c0c0bd..d35e103 100644
--- a/source/ui/OtpView.mc
+++ b/source/ui/OtpView.mc
@@ -131,7 +131,7 @@ class OtpView extends WatchUi.View {
dc.drawText(cx, y, smallFont, Ui.fit(dc, hint, smallFont, 2 * Ui.halfWidthAt(dc, y)), Graphics.TEXT_JUSTIFY_CENTER);
y += smallH;
} else {
- dc.setColor(remaining <= NEXT_CODE_SECONDS ? Ui.WARN_COLOR : Ui.dim(), Graphics.COLOR_TRANSPARENT);
+ dc.setColor(remaining <= NEXT_CODE_SECONDS ? Ui.warn() : Ui.dim(), Graphics.COLOR_TRANSPARENT);
dc.drawText(cx, y, infoFont, remaining + " s", Graphics.TEXT_JUSTIFY_CENTER);
y += dc.getFontHeight(infoFont);
if (remaining <= NEXT_CODE_SECONDS && cached != null) {
@@ -232,7 +232,7 @@ class OtpView extends WatchUi.View {
private function drawTimeLeft(dc as Graphics.Dc, remaining as Number, period as Number) as Void {
var w = dc.getWidth();
var h = dc.getHeight();
- var color = remaining <= NEXT_CODE_SECONDS ? Ui.WARN_COLOR : Ui.accent();
+ var color = remaining <= NEXT_CODE_SECONDS ? Ui.warn() : Ui.accent();
if (Ui.isRound()) {
var r = w / 2 - RING_WIDTH / 2 - 1;
dc.setPenWidth(RING_WIDTH);
diff --git a/source/ui/PinView.mc b/source/ui/PinView.mc
index fa58bfe..47b12fd 100644
--- a/source/ui/PinView.mc
+++ b/source/ui/PinView.mc
@@ -4,7 +4,8 @@
// README 13.1 and 13.4:
// * A PIN has exactly 4 digits (KeyManager.PIN_DIGITS) and is submitted
// as soon as the 4th is entered; there is no OK key.
-// * Touch screens get a keypad (1-9, delete, 0). The START button does
+// * Touch screens get a keypad (1-9, then 0 and delete in the bottom row,
+// delete on the right as on phone keypads). The START button does
// nothing there: a tap can also arrive as START, which used to submit a
// PIN early. Watches without a touch screen get a digit selector:
// UP/DOWN change the digit, START adds it, BACK deletes.
@@ -32,9 +33,11 @@ class PinView extends WatchUi.View {
// Selector positions 0-9 are digits; this one is "OK" (legacy PINs only).
static const SELECT_OK = 10;
- // Keypad keys besides the digits (OK for legacy PINs only).
+ // Keypad keys besides the digits (OK for legacy PINs only), and an
+ // empty slot.
static const KEY_DELETE = -1;
static const KEY_OK = -2;
+ static const KEY_NONE = -3;
// Calibrate the PIN derivation to about this long on this watch.
static const DERIVATION_TARGET_MS = 1000;
static const CALIBRATION_SAMPLE = 200;
@@ -358,7 +361,7 @@ class PinView extends WatchUi.View {
var message = wait > 0 ? "Wrong PINs. Wait " + Ui.minutesSeconds(wait) : _message;
if (message != null) {
- dc.setColor(_messageIsError || wait > 0 ? Ui.WARN_COLOR : Ui.accent(), Graphics.COLOR_TRANSPARENT);
+ dc.setColor(_messageIsError || wait > 0 ? Ui.warn() : Ui.accent(), Graphics.COLOR_TRANSPARENT);
dc.drawText(cx, y, Graphics.FONT_XTINY, Ui.fit(dc, message, Graphics.FONT_XTINY, 2 * Ui.halfWidthAt(dc, y)),
Graphics.TEXT_JUSTIFY_CENTER);
}
@@ -372,6 +375,9 @@ class PinView extends WatchUi.View {
if (!_shownLogged) {
_shownLogged = true;
+ // For UI scripts: the state the dots and the title show (never digits).
+ Diag.data("pin_state", "mode=" + mode + " digits=" + _digits.size() + " repeat=" + (_first != null ? 1 : 0)
+ + " busy=" + (busy ? 1 : 0));
Diag.shown(wait > 0 ? "pin_wait" : (_message != null && _messageIsError ? "pin_error" : "pin"));
}
}
@@ -394,19 +400,21 @@ class PinView extends WatchUi.View {
keyW = keyH * 2;
}
var left = w / 2 - 3 * keyW / 2;
- var labels = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "OK"];
- var values = [1, 2, 3, 4, 5, 6, 7, 8, 9, KEY_DELETE, 0, KEY_OK];
+ // A legacy PIN needs OK: delete, 0, OK. Otherwise the 4th digit
+ // submits, and the bottom row is an empty slot, 0 and delete.
+ var values = legacy ? [1, 2, 3, 4, 5, 6, 7, 8, 9, KEY_DELETE, 0, KEY_OK]
+ : [1, 2, 3, 4, 5, 6, 7, 8, 9, KEY_NONE, 0, KEY_DELETE];
var font = Graphics.FONT_MEDIUM;
_keys = [] as Array<Array<Number>>;
var log = "";
for (var i = 0; i < 12; i++) {
- if (values[i] == KEY_OK && !legacy) {
- continue; // no OK key: the 4th digit submits; the slot stays empty
+ if (values[i] == KEY_NONE) {
+ continue;
}
var kx = left + (i % 3) * keyW;
var ky = top + (i / 3) * keyH;
_keys.add([kx, ky, keyW, keyH, values[i] as Number]);
- var label = labels[i] as String;
+ var label = values[i] == KEY_OK ? "OK" : (values[i] as Number).toString();
var isAction = (values[i] as Number) < 0;
dc.setColor(disabled ? (Ui.dark() ? 0x444444 : Graphics.COLOR_LT_GRAY) : (isAction ? Ui.accent() : Ui.fg()),
Graphics.COLOR_TRANSPARENT);
@@ -416,7 +424,7 @@ class PinView extends WatchUi.View {
var f = isAction ? Graphics.FONT_SMALL : font;
dc.drawText(kx + keyW / 2, ky + (keyH - dc.getFontHeight(f)) / 2, f, label, Graphics.TEXT_JUSTIFY_CENTER);
}
- log += (i > 0 ? ";" : "") + (values[i] as Number) + "=" + (kx + keyW / 2) + "," + (ky + keyH / 2);
+ log += (log.length() > 0 ? ";" : "") + (values[i] as Number) + "=" + (kx + keyW / 2) + "," + (ky + keyH / 2);
}
if (!_shownLogged) {
Diag.data("pin_keys", log);
@@ -444,7 +452,10 @@ class PinView extends WatchUi.View {
private function drawSelector(dc as Graphics.Dc, top as Number, bottom as Number, disabled as Boolean) as Void {
var cx = dc.getWidth() / 2;
- var mid = (top + bottom) / 2;
+ // Centred above the hint, so the lower arrow never touches it (it
+ // did on the Forerunner 255S).
+ var hintTop = bottom - dc.getFontHeight(Graphics.FONT_XTINY);
+ var mid = (top + hintTop) / 2;
var font = Graphics.FONT_NUMBER_MEDIUM;
var label = _selector == SELECT_OK ? "OK" : _selector.toString();
if (_selector == SELECT_OK) {
@@ -458,7 +469,7 @@ class PinView extends WatchUi.View {
dc.fillPolygon([[cx - 8, mid + a], [cx + 8, mid + a], [cx, mid + a + 8]] as Array<Graphics.Point2D>);
dc.setColor(Ui.dim(), Graphics.COLOR_TRANSPARENT);
var hint = "START: add";
- dc.drawText(cx, bottom - dc.getFontHeight(Graphics.FONT_XTINY), Graphics.FONT_XTINY,
+ dc.drawText(cx, hintTop, Graphics.FONT_XTINY,
Ui.fit(dc, hint, Graphics.FONT_XTINY, 2 * Ui.halfWidthAt(dc, bottom)), Graphics.TEXT_JUSTIFY_CENTER);
}
}
diff --git a/source/ui/Ui.mc b/source/ui/Ui.mc
index a8d1ec7..bf52520 100644
--- a/source/ui/Ui.mc
+++ b/source/ui/Ui.mc
@@ -15,6 +15,7 @@ module Ui {
const ELLIPSIS = "…";
const WARN_COLOR = 0xFF5555;
+ const LIGHT_WARN_COLOR = 0xCC0000; // 0xFF5555 is too faint on white for small text
const ACCENT_COLOR = 0x00AAAA; // the teal of the launcher icon
const LIGHT_ACCENT_COLOR = 0x007777;
@@ -71,6 +72,11 @@ module Ui {
return dark() ? ACCENT_COLOR : LIGHT_ACCENT_COLOR;
}
+ // Warnings (wrong PIN, the last seconds of a code) in the theme.
+ function warn() as Number {
+ return dark() ? WARN_COLOR : LIGHT_WARN_COLOR;
+ }
+
// Clear the screen in the theme's background.
function clear(dc as Graphics.Dc) as Void {
dc.setColor(fg(), bg());
@@ -142,7 +148,7 @@ module Ui {
if (period <= 0 || remaining <= 0) {
return;
}
- dc.setColor(remaining <= 5 ? WARN_COLOR : accent(), Graphics.COLOR_TRANSPARENT);
+ dc.setColor(remaining <= 5 ? warn() : accent(), Graphics.COLOR_TRANSPARENT);
var pen = r / 2 > 1 ? r / 2 : 1;
dc.setPenWidth(pen);
var sweep = 360 * remaining / period;
diff --git a/source/util/TestMode.mc b/source/util/TestMode.mc
index e583ecf..6439be1 100644
--- a/source/util/TestMode.mc
+++ b/source/util/TestMode.mc
@@ -1,25 +1,43 @@
// Author: cirakg & claude ai
// Summary: Switch for the UI-test build: a frozen clock, a short auto-lock and a store reset to fake entries.
//
-// Two versions of this module exist; each build keeps exactly one:
+// Three versions of this module exist; each build keeps exactly one:
// * (:no_uitest), in every normal build (debug, release, unit tests):
// ON is false and nothing changes.
-// * (:uitest), only with uitest.jungle (`make screenshots`): the clock is
-// frozen at NOW, so codes and countdowns in screenshots never change; the
-// store is wiped and filled with fake entries at every start
+// * (:uitest), only in the UI-test builds (`make screenshots`): the clock
+// is frozen at NOW, so codes and countdowns in screenshots never change;
+// the store is wiped and filled with fake entries at every start
// (source/app/Fixtures.mc); auto-lock is short enough to test.
+// SCENARIO picks the fixture: SCENARIO_MAIN (uitest.jungle: the import,
+// the forms and the demo entries) or SCENARIO_EXTRA
+// (uitest-extra.jungle: a legacy PIN, a queued "Require PIN", more
+// symbologies and an invalid code), each in its own build, since a
+// launch can't pass arguments to the app.
// The constant-condition checks on ON are resolved by the compiler, so the
// unused branch costs nothing.
import Toybox.Lang;
-(:uitest, :glance)
+(:uitest, :uitest_main, :glance)
module TestMode {
const ON = true;
// 2027-01-15 08:00:26 UTC: 4 s left in a 30 s period, so the detail
// screen shows the next code too.
const NOW = 1800000026l;
const AUTO_LOCK_MS = 10000;
+ const SCENARIO_MAIN = 0;
+ const SCENARIO_EXTRA = 1;
+ const SCENARIO = SCENARIO_MAIN;
+}
+
+(:uitest, :uitest_extra, :glance)
+module TestMode {
+ const ON = true;
+ const NOW = 1800000026l;
+ const AUTO_LOCK_MS = 10000;
+ const SCENARIO_MAIN = 0;
+ const SCENARIO_EXTRA = 1;
+ const SCENARIO = SCENARIO_EXTRA;
}
(:no_uitest, :glance)
@@ -27,4 +45,7 @@ module TestMode {
const ON = false;
const NOW = 0l;
const AUTO_LOCK_MS = 0;
+ const SCENARIO_MAIN = 0;
+ const SCENARIO_EXTRA = 1;
+ const SCENARIO = SCENARIO_MAIN;
}
diff --git a/test/golden/fenix7/clock_status.png b/test/golden/fenix7/clock_status.png
new file mode 100644
index 0000000..8c1df0a
--- /dev/null
+++ b/test/golden/fenix7/clock_status.png
Binary files differ
diff --git a/test/golden/fenix7/code_39.png b/test/golden/fenix7/code_39.png
new file mode 100644
index 0000000..8c6abaa
--- /dev/null
+++ b/test/golden/fenix7/code_39.png
Binary files differ
diff --git a/test/golden/fenix7/code_error.png b/test/golden/fenix7/code_error.png
new file mode 100644
index 0000000..655d6f7
--- /dev/null
+++ b/test/golden/fenix7/code_error.png
Binary files differ
diff --git a/test/golden/fenix7/code_qr_large.png b/test/golden/fenix7/code_qr_large.png
new file mode 100644
index 0000000..ae253f3
--- /dev/null
+++ b/test/golden/fenix7/code_qr_large.png
Binary files differ
diff --git a/test/golden/fenix7/code_upca.png b/test/golden/fenix7/code_upca.png
new file mode 100644
index 0000000..33a281d
--- /dev/null
+++ b/test/golden/fenix7/code_upca.png
Binary files differ
diff --git a/test/golden/fenix7/entry_ids.png b/test/golden/fenix7/entry_ids.png
new file mode 100644
index 0000000..4844c12
--- /dev/null
+++ b/test/golden/fenix7/entry_ids.png
Binary files differ
diff --git a/test/golden/fenix7/entry_ids_next.png b/test/golden/fenix7/entry_ids_next.png
new file mode 100644
index 0000000..9089891
--- /dev/null
+++ b/test/golden/fenix7/entry_ids_next.png
Binary files differ
diff --git a/test/golden/fenix7/glance.png b/test/golden/fenix7/glance.png
new file mode 100644
index 0000000..912a848
--- /dev/null
+++ b/test/golden/fenix7/glance.png
Binary files differ
diff --git a/test/golden/fenix7/list_extra.png b/test/golden/fenix7/list_extra.png
new file mode 100644
index 0000000..b13f652
--- /dev/null
+++ b/test/golden/fenix7/list_extra.png
Binary files differ
diff --git a/test/golden/fenix7/list_locked.png b/test/golden/fenix7/list_locked.png
new file mode 100644
index 0000000..ec0aafd
--- /dev/null
+++ b/test/golden/fenix7/list_locked.png
Binary files differ
diff --git a/test/golden/fenix7/list_reset.png b/test/golden/fenix7/list_reset.png
new file mode 100644
index 0000000..e9e2e8a
--- /dev/null
+++ b/test/golden/fenix7/list_reset.png
Binary files differ
diff --git a/test/golden/fenix7/lock_queued.png b/test/golden/fenix7/lock_queued.png
new file mode 100644
index 0000000..2affaa0
--- /dev/null
+++ b/test/golden/fenix7/lock_queued.png
Binary files differ
diff --git a/test/golden/fenix7/menu_results.png b/test/golden/fenix7/menu_results.png
new file mode 100644
index 0000000..5608acf
--- /dev/null
+++ b/test/golden/fenix7/menu_results.png
Binary files differ
diff --git a/test/golden/fenix7/otp_library.png b/test/golden/fenix7/otp_library.png
new file mode 100644
index 0000000..b2a588f
--- /dev/null
+++ b/test/golden/fenix7/otp_library.png
Binary files differ
diff --git a/test/golden/fenix7/otp_steam.png b/test/golden/fenix7/otp_steam.png
new file mode 100644
index 0000000..a972361
--- /dev/null
+++ b/test/golden/fenix7/otp_steam.png
Binary files differ
diff --git a/test/golden/fenix7/pin.png b/test/golden/fenix7/pin.png
index d1fe80b..5b093dc 100644
--- a/test/golden/fenix7/pin.png
+++ b/test/golden/fenix7/pin.png
Binary files differ
diff --git a/test/golden/fenix7/pin_change.png b/test/golden/fenix7/pin_change.png
new file mode 100644
index 0000000..25cf42f
--- /dev/null
+++ b/test/golden/fenix7/pin_change.png
Binary files differ
diff --git a/test/golden/fenix7/pin_create.png b/test/golden/fenix7/pin_create.png
new file mode 100644
index 0000000..236155c
--- /dev/null
+++ b/test/golden/fenix7/pin_create.png
Binary files differ
diff --git a/test/golden/fenix7/pin_differ.png b/test/golden/fenix7/pin_differ.png
new file mode 100644
index 0000000..f738e6c
--- /dev/null
+++ b/test/golden/fenix7/pin_differ.png
Binary files differ
diff --git a/test/golden/fenix7/pin_error.png b/test/golden/fenix7/pin_error.png
index 48541c8..c3fa1dc 100644
--- a/test/golden/fenix7/pin_error.png
+++ b/test/golden/fenix7/pin_error.png
Binary files differ
diff --git a/test/golden/fenix7/pin_legacy.png b/test/golden/fenix7/pin_legacy.png
new file mode 100644
index 0000000..01286ea
--- /dev/null
+++ b/test/golden/fenix7/pin_legacy.png
Binary files differ
diff --git a/test/golden/fenix7/pin_legacy_typed.png b/test/golden/fenix7/pin_legacy_typed.png
new file mode 100644
index 0000000..7c0b1d7
--- /dev/null
+++ b/test/golden/fenix7/pin_legacy_typed.png
Binary files differ
diff --git a/test/golden/fenix7/pin_new_forced.png b/test/golden/fenix7/pin_new_forced.png
new file mode 100644
index 0000000..9ad113c
--- /dev/null
+++ b/test/golden/fenix7/pin_new_forced.png
Binary files differ
diff --git a/test/golden/fenix7/pin_repeat.png b/test/golden/fenix7/pin_repeat.png
new file mode 100644
index 0000000..8095fc8
--- /dev/null
+++ b/test/golden/fenix7/pin_repeat.png
Binary files differ
diff --git a/test/golden/fenix7/pin_wait.png b/test/golden/fenix7/pin_wait.png
index 77cce98..c856f5c 100644
--- a/test/golden/fenix7/pin_wait.png
+++ b/test/golden/fenix7/pin_wait.png
Binary files differ
diff --git a/test/golden/fenix7/reset_confirm.png b/test/golden/fenix7/reset_confirm.png
new file mode 100644
index 0000000..36b8044
--- /dev/null
+++ b/test/golden/fenix7/reset_confirm.png
Binary files differ
diff --git a/test/golden/fr255s/clock_status.png b/test/golden/fr255s/clock_status.png
new file mode 100644
index 0000000..7c853b4
--- /dev/null
+++ b/test/golden/fr255s/clock_status.png
Binary files differ
diff --git a/test/golden/fr255s/code_39.png b/test/golden/fr255s/code_39.png
new file mode 100644
index 0000000..f566d67
--- /dev/null
+++ b/test/golden/fr255s/code_39.png
Binary files differ
diff --git a/test/golden/fr255s/code_error.png b/test/golden/fr255s/code_error.png
new file mode 100644
index 0000000..de0805f
--- /dev/null
+++ b/test/golden/fr255s/code_error.png
Binary files differ
diff --git a/test/golden/fr255s/code_qr_large.png b/test/golden/fr255s/code_qr_large.png
new file mode 100644
index 0000000..cf9f93f
--- /dev/null
+++ b/test/golden/fr255s/code_qr_large.png
Binary files differ
diff --git a/test/golden/fr255s/code_upca.png b/test/golden/fr255s/code_upca.png
new file mode 100644
index 0000000..79aa52c
--- /dev/null
+++ b/test/golden/fr255s/code_upca.png
Binary files differ
diff --git a/test/golden/fr255s/entry_ids.png b/test/golden/fr255s/entry_ids.png
new file mode 100644
index 0000000..56aef73
--- /dev/null
+++ b/test/golden/fr255s/entry_ids.png
Binary files differ
diff --git a/test/golden/fr255s/entry_ids_next.png b/test/golden/fr255s/entry_ids_next.png
new file mode 100644
index 0000000..46ce47b
--- /dev/null
+++ b/test/golden/fr255s/entry_ids_next.png
Binary files differ
diff --git a/test/golden/fr255s/glance.png b/test/golden/fr255s/glance.png
new file mode 100644
index 0000000..df0dde1
--- /dev/null
+++ b/test/golden/fr255s/glance.png
Binary files differ
diff --git a/test/golden/fr255s/list_extra.png b/test/golden/fr255s/list_extra.png
new file mode 100644
index 0000000..109ad21
--- /dev/null
+++ b/test/golden/fr255s/list_extra.png
Binary files differ
diff --git a/test/golden/fr255s/list_locked.png b/test/golden/fr255s/list_locked.png
new file mode 100644
index 0000000..88624d1
--- /dev/null
+++ b/test/golden/fr255s/list_locked.png
Binary files differ
diff --git a/test/golden/fr255s/list_reset.png b/test/golden/fr255s/list_reset.png
new file mode 100644
index 0000000..66b10c6
--- /dev/null
+++ b/test/golden/fr255s/list_reset.png
Binary files differ
diff --git a/test/golden/fr255s/lock_queued.png b/test/golden/fr255s/lock_queued.png
new file mode 100644
index 0000000..f274a9b
--- /dev/null
+++ b/test/golden/fr255s/lock_queued.png
Binary files differ
diff --git a/test/golden/fr255s/menu_results.png b/test/golden/fr255s/menu_results.png
new file mode 100644
index 0000000..96326d1
--- /dev/null
+++ b/test/golden/fr255s/menu_results.png
Binary files differ
diff --git a/test/golden/fr255s/otp_library.png b/test/golden/fr255s/otp_library.png
new file mode 100644
index 0000000..4126205
--- /dev/null
+++ b/test/golden/fr255s/otp_library.png
Binary files differ
diff --git a/test/golden/fr255s/otp_steam.png b/test/golden/fr255s/otp_steam.png
new file mode 100644
index 0000000..d4e4fe0
--- /dev/null
+++ b/test/golden/fr255s/otp_steam.png
Binary files differ
diff --git a/test/golden/fr255s/pin.png b/test/golden/fr255s/pin.png
index bb3e989..d388b78 100644
--- a/test/golden/fr255s/pin.png
+++ b/test/golden/fr255s/pin.png
Binary files differ
diff --git a/test/golden/fr255s/pin_change.png b/test/golden/fr255s/pin_change.png
new file mode 100644
index 0000000..1cfa707
--- /dev/null
+++ b/test/golden/fr255s/pin_change.png
Binary files differ
diff --git a/test/golden/fr255s/pin_create.png b/test/golden/fr255s/pin_create.png
new file mode 100644
index 0000000..b89194e
--- /dev/null
+++ b/test/golden/fr255s/pin_create.png
Binary files differ
diff --git a/test/golden/fr255s/pin_differ.png b/test/golden/fr255s/pin_differ.png
new file mode 100644
index 0000000..0e3de3a
--- /dev/null
+++ b/test/golden/fr255s/pin_differ.png
Binary files differ
diff --git a/test/golden/fr255s/pin_error.png b/test/golden/fr255s/pin_error.png
index b580338..320220e 100644
--- a/test/golden/fr255s/pin_error.png
+++ b/test/golden/fr255s/pin_error.png
Binary files differ
diff --git a/test/golden/fr255s/pin_legacy.png b/test/golden/fr255s/pin_legacy.png
new file mode 100644
index 0000000..f7ee814
--- /dev/null
+++ b/test/golden/fr255s/pin_legacy.png
Binary files differ
diff --git a/test/golden/fr255s/pin_legacy_typed.png b/test/golden/fr255s/pin_legacy_typed.png
new file mode 100644
index 0000000..84cbb8f
--- /dev/null
+++ b/test/golden/fr255s/pin_legacy_typed.png
Binary files differ
diff --git a/test/golden/fr255s/pin_new_forced.png b/test/golden/fr255s/pin_new_forced.png
new file mode 100644
index 0000000..06d1974
--- /dev/null
+++ b/test/golden/fr255s/pin_new_forced.png
Binary files differ
diff --git a/test/golden/fr255s/pin_repeat.png b/test/golden/fr255s/pin_repeat.png
new file mode 100644
index 0000000..7e69048
--- /dev/null
+++ b/test/golden/fr255s/pin_repeat.png
Binary files differ
diff --git a/test/golden/fr255s/pin_wait.png b/test/golden/fr255s/pin_wait.png
index cc1e27b..b584db2 100644
--- a/test/golden/fr255s/pin_wait.png
+++ b/test/golden/fr255s/pin_wait.png
Binary files differ
diff --git a/test/golden/fr255s/reset_confirm.png b/test/golden/fr255s/reset_confirm.png
new file mode 100644
index 0000000..f90cfc4
--- /dev/null
+++ b/test/golden/fr255s/reset_confirm.png
Binary files differ
diff --git a/test/golden/fr70/about.png b/test/golden/fr70/about.png
index c92b268..7636e45 100644
--- a/test/golden/fr70/about.png
+++ b/test/golden/fr70/about.png
Binary files differ
diff --git a/test/golden/fr70/clock_status.png b/test/golden/fr70/clock_status.png
new file mode 100644
index 0000000..bac3d93
--- /dev/null
+++ b/test/golden/fr70/clock_status.png
Binary files differ
diff --git a/test/golden/fr70/code_39.png b/test/golden/fr70/code_39.png
new file mode 100644
index 0000000..5b12d54
--- /dev/null
+++ b/test/golden/fr70/code_39.png
Binary files differ
diff --git a/test/golden/fr70/code_error.png b/test/golden/fr70/code_error.png
new file mode 100644
index 0000000..77255c9
--- /dev/null
+++ b/test/golden/fr70/code_error.png
Binary files differ
diff --git a/test/golden/fr70/code_qr_large.png b/test/golden/fr70/code_qr_large.png
new file mode 100644
index 0000000..635e19c
--- /dev/null
+++ b/test/golden/fr70/code_qr_large.png
Binary files differ
diff --git a/test/golden/fr70/code_upca.png b/test/golden/fr70/code_upca.png
new file mode 100644
index 0000000..a08c3b5
--- /dev/null
+++ b/test/golden/fr70/code_upca.png
Binary files differ
diff --git a/test/golden/fr70/entry_ids.png b/test/golden/fr70/entry_ids.png
new file mode 100644
index 0000000..c021f2a
--- /dev/null
+++ b/test/golden/fr70/entry_ids.png
Binary files differ
diff --git a/test/golden/fr70/entry_ids_next.png b/test/golden/fr70/entry_ids_next.png
new file mode 100644
index 0000000..c61c6cd
--- /dev/null
+++ b/test/golden/fr70/entry_ids_next.png
Binary files differ
diff --git a/test/golden/fr70/glance.png b/test/golden/fr70/glance.png
new file mode 100644
index 0000000..7d56b74
--- /dev/null
+++ b/test/golden/fr70/glance.png
Binary files differ
diff --git a/test/golden/fr70/list_extra.png b/test/golden/fr70/list_extra.png
new file mode 100644
index 0000000..207a7de
--- /dev/null
+++ b/test/golden/fr70/list_extra.png
Binary files differ
diff --git a/test/golden/fr70/list_locked.png b/test/golden/fr70/list_locked.png
new file mode 100644
index 0000000..2981ae9
--- /dev/null
+++ b/test/golden/fr70/list_locked.png
Binary files differ
diff --git a/test/golden/fr70/list_reset.png b/test/golden/fr70/list_reset.png
new file mode 100644
index 0000000..7b6f369
--- /dev/null
+++ b/test/golden/fr70/list_reset.png
Binary files differ
diff --git a/test/golden/fr70/lock_queued.png b/test/golden/fr70/lock_queued.png
new file mode 100644
index 0000000..b81677b
--- /dev/null
+++ b/test/golden/fr70/lock_queued.png
Binary files differ
diff --git a/test/golden/fr70/menu_results.png b/test/golden/fr70/menu_results.png
new file mode 100644
index 0000000..eb9cb8c
--- /dev/null
+++ b/test/golden/fr70/menu_results.png
Binary files differ
diff --git a/test/golden/fr70/otp_library.png b/test/golden/fr70/otp_library.png
new file mode 100644
index 0000000..50c4dcc
--- /dev/null
+++ b/test/golden/fr70/otp_library.png
Binary files differ
diff --git a/test/golden/fr70/otp_steam.png b/test/golden/fr70/otp_steam.png
new file mode 100644
index 0000000..6a90090
--- /dev/null
+++ b/test/golden/fr70/otp_steam.png
Binary files differ
diff --git a/test/golden/fr70/pin.png b/test/golden/fr70/pin.png
index 71f6579..1c7863e 100644
--- a/test/golden/fr70/pin.png
+++ b/test/golden/fr70/pin.png
Binary files differ
diff --git a/test/golden/fr70/pin_change.png b/test/golden/fr70/pin_change.png
new file mode 100644
index 0000000..e52dec5
--- /dev/null
+++ b/test/golden/fr70/pin_change.png
Binary files differ
diff --git a/test/golden/fr70/pin_create.png b/test/golden/fr70/pin_create.png
new file mode 100644
index 0000000..96dc179
--- /dev/null
+++ b/test/golden/fr70/pin_create.png
Binary files differ
diff --git a/test/golden/fr70/pin_differ.png b/test/golden/fr70/pin_differ.png
new file mode 100644
index 0000000..a8966ea
--- /dev/null
+++ b/test/golden/fr70/pin_differ.png
Binary files differ
diff --git a/test/golden/fr70/pin_error.png b/test/golden/fr70/pin_error.png
index 9a3de60..aab9982 100644
--- a/test/golden/fr70/pin_error.png
+++ b/test/golden/fr70/pin_error.png
Binary files differ
diff --git a/test/golden/fr70/pin_legacy.png b/test/golden/fr70/pin_legacy.png
new file mode 100644
index 0000000..d103e86
--- /dev/null
+++ b/test/golden/fr70/pin_legacy.png
Binary files differ
diff --git a/test/golden/fr70/pin_legacy_typed.png b/test/golden/fr70/pin_legacy_typed.png
new file mode 100644
index 0000000..d8e7038
--- /dev/null
+++ b/test/golden/fr70/pin_legacy_typed.png
Binary files differ
diff --git a/test/golden/fr70/pin_new_forced.png b/test/golden/fr70/pin_new_forced.png
new file mode 100644
index 0000000..ced0c5b
--- /dev/null
+++ b/test/golden/fr70/pin_new_forced.png
Binary files differ
diff --git a/test/golden/fr70/pin_repeat.png b/test/golden/fr70/pin_repeat.png
new file mode 100644
index 0000000..574bda6
--- /dev/null
+++ b/test/golden/fr70/pin_repeat.png
Binary files differ
diff --git a/test/golden/fr70/pin_wait.png b/test/golden/fr70/pin_wait.png
index ed8132e..9b726f8 100644
--- a/test/golden/fr70/pin_wait.png
+++ b/test/golden/fr70/pin_wait.png
Binary files differ
diff --git a/test/golden/fr70/reset_confirm.png b/test/golden/fr70/reset_confirm.png
new file mode 100644
index 0000000..56351e9
--- /dev/null
+++ b/test/golden/fr70/reset_confirm.png
Binary files differ
diff --git a/test/golden/fr965/clock_status.png b/test/golden/fr965/clock_status.png
new file mode 100644
index 0000000..2b0a7c2
--- /dev/null
+++ b/test/golden/fr965/clock_status.png
Binary files differ
diff --git a/test/golden/fr965/code_39.png b/test/golden/fr965/code_39.png
new file mode 100644
index 0000000..fd62d3e
--- /dev/null
+++ b/test/golden/fr965/code_39.png
Binary files differ
diff --git a/test/golden/fr965/code_error.png b/test/golden/fr965/code_error.png
new file mode 100644
index 0000000..6601baf
--- /dev/null
+++ b/test/golden/fr965/code_error.png
Binary files differ
diff --git a/test/golden/fr965/code_qr_large.png b/test/golden/fr965/code_qr_large.png
new file mode 100644
index 0000000..69fea25
--- /dev/null
+++ b/test/golden/fr965/code_qr_large.png
Binary files differ
diff --git a/test/golden/fr965/code_upca.png b/test/golden/fr965/code_upca.png
new file mode 100644
index 0000000..45b4ac5
--- /dev/null
+++ b/test/golden/fr965/code_upca.png
Binary files differ
diff --git a/test/golden/fr965/entry_ids.png b/test/golden/fr965/entry_ids.png
new file mode 100644
index 0000000..cbd911f
--- /dev/null
+++ b/test/golden/fr965/entry_ids.png
Binary files differ
diff --git a/test/golden/fr965/entry_ids_next.png b/test/golden/fr965/entry_ids_next.png
new file mode 100644
index 0000000..0bb5206
--- /dev/null
+++ b/test/golden/fr965/entry_ids_next.png
Binary files differ
diff --git a/test/golden/fr965/glance.png b/test/golden/fr965/glance.png
new file mode 100644
index 0000000..8aa9eca
--- /dev/null
+++ b/test/golden/fr965/glance.png
Binary files differ
diff --git a/test/golden/fr965/list_extra.png b/test/golden/fr965/list_extra.png
new file mode 100644
index 0000000..cdee2ed
--- /dev/null
+++ b/test/golden/fr965/list_extra.png
Binary files differ
diff --git a/test/golden/fr965/list_locked.png b/test/golden/fr965/list_locked.png
new file mode 100644
index 0000000..55fb2c1
--- /dev/null
+++ b/test/golden/fr965/list_locked.png
Binary files differ
diff --git a/test/golden/fr965/list_reset.png b/test/golden/fr965/list_reset.png
new file mode 100644
index 0000000..8df05f9
--- /dev/null
+++ b/test/golden/fr965/list_reset.png
Binary files differ
diff --git a/test/golden/fr965/lock_queued.png b/test/golden/fr965/lock_queued.png
new file mode 100644
index 0000000..8cdd66b
--- /dev/null
+++ b/test/golden/fr965/lock_queued.png
Binary files differ
diff --git a/test/golden/fr965/menu_results.png b/test/golden/fr965/menu_results.png
new file mode 100644
index 0000000..1781e6e
--- /dev/null
+++ b/test/golden/fr965/menu_results.png
Binary files differ
diff --git a/test/golden/fr965/otp_library.png b/test/golden/fr965/otp_library.png
new file mode 100644
index 0000000..d19ecb3
--- /dev/null
+++ b/test/golden/fr965/otp_library.png
Binary files differ
diff --git a/test/golden/fr965/otp_steam.png b/test/golden/fr965/otp_steam.png
new file mode 100644
index 0000000..3bb4d79
--- /dev/null
+++ b/test/golden/fr965/otp_steam.png
Binary files differ
diff --git a/test/golden/fr965/pin.png b/test/golden/fr965/pin.png
index 06cab65..a85643e 100644
--- a/test/golden/fr965/pin.png
+++ b/test/golden/fr965/pin.png
Binary files differ
diff --git a/test/golden/fr965/pin_change.png b/test/golden/fr965/pin_change.png
new file mode 100644
index 0000000..2beee26
--- /dev/null
+++ b/test/golden/fr965/pin_change.png
Binary files differ
diff --git a/test/golden/fr965/pin_create.png b/test/golden/fr965/pin_create.png
new file mode 100644
index 0000000..808253b
--- /dev/null
+++ b/test/golden/fr965/pin_create.png
Binary files differ
diff --git a/test/golden/fr965/pin_differ.png b/test/golden/fr965/pin_differ.png
new file mode 100644
index 0000000..50b4fa1
--- /dev/null
+++ b/test/golden/fr965/pin_differ.png
Binary files differ
diff --git a/test/golden/fr965/pin_error.png b/test/golden/fr965/pin_error.png
index c187ada..319cf13 100644
--- a/test/golden/fr965/pin_error.png
+++ b/test/golden/fr965/pin_error.png
Binary files differ
diff --git a/test/golden/fr965/pin_legacy.png b/test/golden/fr965/pin_legacy.png
new file mode 100644
index 0000000..c3ecd06
--- /dev/null
+++ b/test/golden/fr965/pin_legacy.png
Binary files differ
diff --git a/test/golden/fr965/pin_legacy_typed.png b/test/golden/fr965/pin_legacy_typed.png
new file mode 100644
index 0000000..788feaf
--- /dev/null
+++ b/test/golden/fr965/pin_legacy_typed.png
Binary files differ
diff --git a/test/golden/fr965/pin_new_forced.png b/test/golden/fr965/pin_new_forced.png
new file mode 100644
index 0000000..e83c73f
--- /dev/null
+++ b/test/golden/fr965/pin_new_forced.png
Binary files differ
diff --git a/test/golden/fr965/pin_repeat.png b/test/golden/fr965/pin_repeat.png
new file mode 100644
index 0000000..9a60945
--- /dev/null
+++ b/test/golden/fr965/pin_repeat.png
Binary files differ
diff --git a/test/golden/fr965/pin_wait.png b/test/golden/fr965/pin_wait.png
index e381d23..24c2a33 100644
--- a/test/golden/fr965/pin_wait.png
+++ b/test/golden/fr965/pin_wait.png
Binary files differ
diff --git a/test/golden/fr965/reset_confirm.png b/test/golden/fr965/reset_confirm.png
new file mode 100644
index 0000000..faa5fd1
--- /dev/null
+++ b/test/golden/fr965/reset_confirm.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/clock_status.png b/test/golden/instinct3amoled45mm/clock_status.png
new file mode 100644
index 0000000..d7dd009
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/clock_status.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/code_39.png b/test/golden/instinct3amoled45mm/code_39.png
new file mode 100644
index 0000000..4403ad8
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/code_39.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/code_error.png b/test/golden/instinct3amoled45mm/code_error.png
new file mode 100644
index 0000000..a4f9ee8
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/code_error.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/code_qr_large.png b/test/golden/instinct3amoled45mm/code_qr_large.png
new file mode 100644
index 0000000..ddc19da
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/code_qr_large.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/code_upca.png b/test/golden/instinct3amoled45mm/code_upca.png
new file mode 100644
index 0000000..7ee5625
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/code_upca.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/entry_ids.png b/test/golden/instinct3amoled45mm/entry_ids.png
new file mode 100644
index 0000000..17b6fc6
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/entry_ids.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/entry_ids_next.png b/test/golden/instinct3amoled45mm/entry_ids_next.png
new file mode 100644
index 0000000..be04703
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/entry_ids_next.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/glance.png b/test/golden/instinct3amoled45mm/glance.png
new file mode 100644
index 0000000..94fcfb9
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/glance.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/list_extra.png b/test/golden/instinct3amoled45mm/list_extra.png
new file mode 100644
index 0000000..a7d2ca9
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/list_extra.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/list_locked.png b/test/golden/instinct3amoled45mm/list_locked.png
new file mode 100644
index 0000000..3a6a6e1
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/list_locked.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/list_reset.png b/test/golden/instinct3amoled45mm/list_reset.png
new file mode 100644
index 0000000..19030d3
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/list_reset.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/lock_queued.png b/test/golden/instinct3amoled45mm/lock_queued.png
new file mode 100644
index 0000000..4fbdcb1
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/lock_queued.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/menu_results.png b/test/golden/instinct3amoled45mm/menu_results.png
new file mode 100644
index 0000000..2f5da34
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/menu_results.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/otp_library.png b/test/golden/instinct3amoled45mm/otp_library.png
new file mode 100644
index 0000000..5896364
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/otp_library.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/otp_steam.png b/test/golden/instinct3amoled45mm/otp_steam.png
new file mode 100644
index 0000000..9f21ae0
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/otp_steam.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/pin.png b/test/golden/instinct3amoled45mm/pin.png
index 92b60d5..79cad73 100644
--- a/test/golden/instinct3amoled45mm/pin.png
+++ b/test/golden/instinct3amoled45mm/pin.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/pin_change.png b/test/golden/instinct3amoled45mm/pin_change.png
new file mode 100644
index 0000000..8d5f742
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/pin_change.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/pin_create.png b/test/golden/instinct3amoled45mm/pin_create.png
new file mode 100644
index 0000000..1010339
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/pin_create.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/pin_differ.png b/test/golden/instinct3amoled45mm/pin_differ.png
new file mode 100644
index 0000000..26969d8
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/pin_differ.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/pin_error.png b/test/golden/instinct3amoled45mm/pin_error.png
index 9d330b8..2c8a922 100644
--- a/test/golden/instinct3amoled45mm/pin_error.png
+++ b/test/golden/instinct3amoled45mm/pin_error.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/pin_legacy.png b/test/golden/instinct3amoled45mm/pin_legacy.png
new file mode 100644
index 0000000..5619fd9
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/pin_legacy.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/pin_legacy_typed.png b/test/golden/instinct3amoled45mm/pin_legacy_typed.png
new file mode 100644
index 0000000..d127e7e
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/pin_legacy_typed.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/pin_new_forced.png b/test/golden/instinct3amoled45mm/pin_new_forced.png
new file mode 100644
index 0000000..7f8b91e
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/pin_new_forced.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/pin_repeat.png b/test/golden/instinct3amoled45mm/pin_repeat.png
new file mode 100644
index 0000000..8c2fb9a
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/pin_repeat.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/pin_wait.png b/test/golden/instinct3amoled45mm/pin_wait.png
index 39eae8d..a6d2141 100644
--- a/test/golden/instinct3amoled45mm/pin_wait.png
+++ b/test/golden/instinct3amoled45mm/pin_wait.png
Binary files differ
diff --git a/test/golden/instinct3amoled45mm/reset_confirm.png b/test/golden/instinct3amoled45mm/reset_confirm.png
new file mode 100644
index 0000000..38cf4da
--- /dev/null
+++ b/test/golden/instinct3amoled45mm/reset_confirm.png
Binary files differ
diff --git a/test/golden/venusq2/about.png b/test/golden/venusq2/about.png
new file mode 100644
index 0000000..14f3947
--- /dev/null
+++ b/test/golden/venusq2/about.png
Binary files differ
diff --git a/test/golden/venusq2/clock_status.png b/test/golden/venusq2/clock_status.png
new file mode 100644
index 0000000..bb374a4
--- /dev/null
+++ b/test/golden/venusq2/clock_status.png
Binary files differ
diff --git a/test/golden/venusq2/code_39.png b/test/golden/venusq2/code_39.png
new file mode 100644
index 0000000..70ee369
--- /dev/null
+++ b/test/golden/venusq2/code_39.png
Binary files differ
diff --git a/test/golden/venusq2/code_error.png b/test/golden/venusq2/code_error.png
new file mode 100644
index 0000000..13ffbbe
--- /dev/null
+++ b/test/golden/venusq2/code_error.png
Binary files differ
diff --git a/test/golden/venusq2/code_qr_large.png b/test/golden/venusq2/code_qr_large.png
new file mode 100644
index 0000000..24a0101
--- /dev/null
+++ b/test/golden/venusq2/code_qr_large.png
Binary files differ
diff --git a/test/golden/venusq2/code_upca.png b/test/golden/venusq2/code_upca.png
new file mode 100644
index 0000000..1371573
--- /dev/null
+++ b/test/golden/venusq2/code_upca.png
Binary files differ
diff --git a/test/golden/venusq2/entry_ids.png b/test/golden/venusq2/entry_ids.png
new file mode 100644
index 0000000..db31b32
--- /dev/null
+++ b/test/golden/venusq2/entry_ids.png
Binary files differ
diff --git a/test/golden/venusq2/entry_ids_next.png b/test/golden/venusq2/entry_ids_next.png
new file mode 100644
index 0000000..f667d36
--- /dev/null
+++ b/test/golden/venusq2/entry_ids_next.png
Binary files differ
diff --git a/test/golden/venusq2/glance.png b/test/golden/venusq2/glance.png
new file mode 100644
index 0000000..fd0f0de
--- /dev/null
+++ b/test/golden/venusq2/glance.png
Binary files differ
diff --git a/test/golden/venusq2/import_results.png b/test/golden/venusq2/import_results.png
index d908572..9182911 100644
--- a/test/golden/venusq2/import_results.png
+++ b/test/golden/venusq2/import_results.png
Binary files differ
diff --git a/test/golden/venusq2/list_extra.png b/test/golden/venusq2/list_extra.png
new file mode 100644
index 0000000..6c905ca
--- /dev/null
+++ b/test/golden/venusq2/list_extra.png
Binary files differ
diff --git a/test/golden/venusq2/list_locked.png b/test/golden/venusq2/list_locked.png
new file mode 100644
index 0000000..2463f99
--- /dev/null
+++ b/test/golden/venusq2/list_locked.png
Binary files differ
diff --git a/test/golden/venusq2/list_reset.png b/test/golden/venusq2/list_reset.png
new file mode 100644
index 0000000..755bb20
--- /dev/null
+++ b/test/golden/venusq2/list_reset.png
Binary files differ
diff --git a/test/golden/venusq2/lock_queued.png b/test/golden/venusq2/lock_queued.png
new file mode 100644
index 0000000..b72824a
--- /dev/null
+++ b/test/golden/venusq2/lock_queued.png
Binary files differ
diff --git a/test/golden/venusq2/menu_results.png b/test/golden/venusq2/menu_results.png
new file mode 100644
index 0000000..714cbd5
--- /dev/null
+++ b/test/golden/venusq2/menu_results.png
Binary files differ
diff --git a/test/golden/venusq2/otp_library.png b/test/golden/venusq2/otp_library.png
new file mode 100644
index 0000000..84e7972
--- /dev/null
+++ b/test/golden/venusq2/otp_library.png
Binary files differ
diff --git a/test/golden/venusq2/otp_steam.png b/test/golden/venusq2/otp_steam.png
new file mode 100644
index 0000000..6cc6c02
--- /dev/null
+++ b/test/golden/venusq2/otp_steam.png
Binary files differ
diff --git a/test/golden/venusq2/pin.png b/test/golden/venusq2/pin.png
index 9b51943..ef526ff 100644
--- a/test/golden/venusq2/pin.png
+++ b/test/golden/venusq2/pin.png
Binary files differ
diff --git a/test/golden/venusq2/pin_change.png b/test/golden/venusq2/pin_change.png
new file mode 100644
index 0000000..795d24d
--- /dev/null
+++ b/test/golden/venusq2/pin_change.png
Binary files differ
diff --git a/test/golden/venusq2/pin_create.png b/test/golden/venusq2/pin_create.png
new file mode 100644
index 0000000..d1d55b2
--- /dev/null
+++ b/test/golden/venusq2/pin_create.png
Binary files differ
diff --git a/test/golden/venusq2/pin_differ.png b/test/golden/venusq2/pin_differ.png
new file mode 100644
index 0000000..3a79ecb
--- /dev/null
+++ b/test/golden/venusq2/pin_differ.png
Binary files differ
diff --git a/test/golden/venusq2/pin_error.png b/test/golden/venusq2/pin_error.png
index 80a803e..ec58981 100644
--- a/test/golden/venusq2/pin_error.png
+++ b/test/golden/venusq2/pin_error.png
Binary files differ
diff --git a/test/golden/venusq2/pin_legacy.png b/test/golden/venusq2/pin_legacy.png
new file mode 100644
index 0000000..49a57b1
--- /dev/null
+++ b/test/golden/venusq2/pin_legacy.png
Binary files differ
diff --git a/test/golden/venusq2/pin_legacy_typed.png b/test/golden/venusq2/pin_legacy_typed.png
new file mode 100644
index 0000000..e9f3096
--- /dev/null
+++ b/test/golden/venusq2/pin_legacy_typed.png
Binary files differ
diff --git a/test/golden/venusq2/pin_new_forced.png b/test/golden/venusq2/pin_new_forced.png
new file mode 100644
index 0000000..4951be0
--- /dev/null
+++ b/test/golden/venusq2/pin_new_forced.png
Binary files differ
diff --git a/test/golden/venusq2/pin_repeat.png b/test/golden/venusq2/pin_repeat.png
new file mode 100644
index 0000000..d87c7f1
--- /dev/null
+++ b/test/golden/venusq2/pin_repeat.png
Binary files differ
diff --git a/test/golden/venusq2/pin_wait.png b/test/golden/venusq2/pin_wait.png
index 618e517..58a4f27 100644
--- a/test/golden/venusq2/pin_wait.png
+++ b/test/golden/venusq2/pin_wait.png
Binary files differ
diff --git a/test/golden/venusq2/reset_confirm.png b/test/golden/venusq2/reset_confirm.png
new file mode 100644
index 0000000..ec0dba2
--- /dev/null
+++ b/test/golden/venusq2/reset_confirm.png
Binary files differ
diff --git a/test/golden/venux1/about.png b/test/golden/venux1/about.png
new file mode 100644
index 0000000..0f0cca3
--- /dev/null
+++ b/test/golden/venux1/about.png
Binary files differ
diff --git a/test/golden/venux1/clock_status.png b/test/golden/venux1/clock_status.png
new file mode 100644
index 0000000..20347e7
--- /dev/null
+++ b/test/golden/venux1/clock_status.png
Binary files differ
diff --git a/test/golden/venux1/code_39.png b/test/golden/venux1/code_39.png
new file mode 100644
index 0000000..0e13911
--- /dev/null
+++ b/test/golden/venux1/code_39.png
Binary files differ
diff --git a/test/golden/venux1/code_error.png b/test/golden/venux1/code_error.png
new file mode 100644
index 0000000..fc90fa8
--- /dev/null
+++ b/test/golden/venux1/code_error.png
Binary files differ
diff --git a/test/golden/venux1/code_qr_large.png b/test/golden/venux1/code_qr_large.png
new file mode 100644
index 0000000..e024b46
--- /dev/null
+++ b/test/golden/venux1/code_qr_large.png
Binary files differ
diff --git a/test/golden/venux1/code_upca.png b/test/golden/venux1/code_upca.png
new file mode 100644
index 0000000..1a2c8f9
--- /dev/null
+++ b/test/golden/venux1/code_upca.png
Binary files differ
diff --git a/test/golden/venux1/entry_ids.png b/test/golden/venux1/entry_ids.png
new file mode 100644
index 0000000..9d7baae
--- /dev/null
+++ b/test/golden/venux1/entry_ids.png
Binary files differ
diff --git a/test/golden/venux1/entry_ids_next.png b/test/golden/venux1/entry_ids_next.png
new file mode 100644
index 0000000..f3f237d
--- /dev/null
+++ b/test/golden/venux1/entry_ids_next.png
Binary files differ
diff --git a/test/golden/venux1/glance.png b/test/golden/venux1/glance.png
new file mode 100644
index 0000000..8f81071
--- /dev/null
+++ b/test/golden/venux1/glance.png
Binary files differ
diff --git a/test/golden/venux1/import_results.png b/test/golden/venux1/import_results.png
index 3f27b17..faeca2d 100644
--- a/test/golden/venux1/import_results.png
+++ b/test/golden/venux1/import_results.png
Binary files differ
diff --git a/test/golden/venux1/list_extra.png b/test/golden/venux1/list_extra.png
new file mode 100644
index 0000000..793192d
--- /dev/null
+++ b/test/golden/venux1/list_extra.png
Binary files differ
diff --git a/test/golden/venux1/list_locked.png b/test/golden/venux1/list_locked.png
new file mode 100644
index 0000000..6842136
--- /dev/null
+++ b/test/golden/venux1/list_locked.png
Binary files differ
diff --git a/test/golden/venux1/list_reset.png b/test/golden/venux1/list_reset.png
new file mode 100644
index 0000000..8ec9498
--- /dev/null
+++ b/test/golden/venux1/list_reset.png
Binary files differ
diff --git a/test/golden/venux1/lock_queued.png b/test/golden/venux1/lock_queued.png
new file mode 100644
index 0000000..e9717f4
--- /dev/null
+++ b/test/golden/venux1/lock_queued.png
Binary files differ
diff --git a/test/golden/venux1/menu_results.png b/test/golden/venux1/menu_results.png
new file mode 100644
index 0000000..8512577
--- /dev/null
+++ b/test/golden/venux1/menu_results.png
Binary files differ
diff --git a/test/golden/venux1/otp_library.png b/test/golden/venux1/otp_library.png
new file mode 100644
index 0000000..4707b71
--- /dev/null
+++ b/test/golden/venux1/otp_library.png
Binary files differ
diff --git a/test/golden/venux1/otp_steam.png b/test/golden/venux1/otp_steam.png
new file mode 100644
index 0000000..3810a3a
--- /dev/null
+++ b/test/golden/venux1/otp_steam.png
Binary files differ
diff --git a/test/golden/venux1/pin.png b/test/golden/venux1/pin.png
index 6263938..9100cc4 100644
--- a/test/golden/venux1/pin.png
+++ b/test/golden/venux1/pin.png
Binary files differ
diff --git a/test/golden/venux1/pin_change.png b/test/golden/venux1/pin_change.png
new file mode 100644
index 0000000..ae8ff2b
--- /dev/null
+++ b/test/golden/venux1/pin_change.png
Binary files differ
diff --git a/test/golden/venux1/pin_create.png b/test/golden/venux1/pin_create.png
new file mode 100644
index 0000000..e29e072
--- /dev/null
+++ b/test/golden/venux1/pin_create.png
Binary files differ
diff --git a/test/golden/venux1/pin_differ.png b/test/golden/venux1/pin_differ.png
new file mode 100644
index 0000000..69f32e4
--- /dev/null
+++ b/test/golden/venux1/pin_differ.png
Binary files differ
diff --git a/test/golden/venux1/pin_error.png b/test/golden/venux1/pin_error.png
index ff2e723..c0a9029 100644
--- a/test/golden/venux1/pin_error.png
+++ b/test/golden/venux1/pin_error.png
Binary files differ
diff --git a/test/golden/venux1/pin_legacy.png b/test/golden/venux1/pin_legacy.png
new file mode 100644
index 0000000..b5e7e23
--- /dev/null
+++ b/test/golden/venux1/pin_legacy.png
Binary files differ
diff --git a/test/golden/venux1/pin_legacy_typed.png b/test/golden/venux1/pin_legacy_typed.png
new file mode 100644
index 0000000..74c79ad
--- /dev/null
+++ b/test/golden/venux1/pin_legacy_typed.png
Binary files differ
diff --git a/test/golden/venux1/pin_new_forced.png b/test/golden/venux1/pin_new_forced.png
new file mode 100644
index 0000000..948a890
--- /dev/null
+++ b/test/golden/venux1/pin_new_forced.png
Binary files differ
diff --git a/test/golden/venux1/pin_repeat.png b/test/golden/venux1/pin_repeat.png
new file mode 100644
index 0000000..b3b8858
--- /dev/null
+++ b/test/golden/venux1/pin_repeat.png
Binary files differ
diff --git a/test/golden/venux1/pin_wait.png b/test/golden/venux1/pin_wait.png
index c91126f..d253871 100644
--- a/test/golden/venux1/pin_wait.png
+++ b/test/golden/venux1/pin_wait.png
Binary files differ
diff --git a/test/golden/venux1/reset_confirm.png b/test/golden/venux1/reset_confirm.png
new file mode 100644
index 0000000..31c2030
--- /dev/null
+++ b/test/golden/venux1/reset_confirm.png
Binary files differ
diff --git a/tools/gen_fixture_wrap.py b/tools/gen_fixture_wrap.py
index 6eb207a..9daca94 100644
--- a/tools/gen_fixture_wrap.py
+++ b/tools/gen_fixture_wrap.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Author: cirakg & claude ai
-# Summary: Compute the UI-test fixture's PIN wrap (fake PIN 1234) so the watch needn't run PBKDF2 at start-up.
+# Summary: Compute the UI-test fixtures' PIN wraps (fake PINs 1234 and legacy 123456): no PBKDF2 on the watch at start.
"""Print the fake PIN wrap that source/app/Fixtures.mc stores in the UI-test build.
The wrap follows docs/SECURITY.md ("PIN wrap, version 1") with fixed, fake
@@ -11,8 +11,12 @@ the fixture stores this precomputed value instead. The app's own unlock code
then checks it like any other wrap, which also cross-checks the format.
The fixture also stores the 4-digit PIN marker (KeyManager.PIN_LENGTH_KEY =
PIN_DIGITS), so the PIN isn't treated as a legacy 4-to-10-digit one.
+The extra UI-test scenario stores LEGACY_WRAP instead, the same master key
+under the 6-digit PIN 123456 and without the marker: a PIN from before the
+4-digit rule, which unlocks with OK and is then replaced.
-Usage: gen_fixture_wrap.py (prints FIXTURE_PIN, FIXTURE_WRAP and FIXTURE_MASTER_KEY in hex,
+Usage: gen_fixture_wrap.py (prints FIXTURE_PIN, FIXTURE_WRAP, FIXTURE_MASTER_KEY,
+ FIXTURE_LEGACY_PIN and FIXTURE_LEGACY_WRAP in hex,
and the marker the fixture stores)
"""
@@ -24,6 +28,7 @@ import struct
import subprocess
PIN = b"1234"
+LEGACY_PIN = b"123456" # the extra scenario's PIN from before the 4-digit rule
PIN_DIGITS = 4 # source/storage/KeyManager.mc: every new PIN, and the marker value
PIN_LENGTH_KEY = "wl" # source/storage/KeyManager.mc
ITERATIONS = 2000
@@ -61,8 +66,8 @@ def sealed_box(enc_key: bytes, mac_key: bytes, aad: bytes, plaintext: bytes) ->
return body + tag
-def fixture_wrap() -> bytes:
- pin_key = hashlib.pbkdf2_hmac("sha256", PIN, SALT, ITERATIONS, 32)
+def fixture_wrap(pin: bytes = PIN) -> bytes:
+ pin_key = hashlib.pbkdf2_hmac("sha256", pin, SALT, ITERATIONS, 32)
enc_key, mac_key = hkdf(pin_key, b"OTPW enc v1"), hkdf(pin_key, b"OTPW mac v1")
count = struct.pack(">I", ITERATIONS)
aad = b"OTPW-WRAP-1" + SALT + count
@@ -74,6 +79,8 @@ def main() -> None:
print("FIXTURE_PIN =", PIN.decode())
print("FIXTURE_WRAP =", fixture_wrap().hex())
print("FIXTURE_MASTER_KEY =", MASTER_KEY.hex())
+ print("FIXTURE_LEGACY_PIN =", LEGACY_PIN.decode())
+ print("FIXTURE_LEGACY_WRAP =", fixture_wrap(LEGACY_PIN).hex())
print(f"marker: {PIN_LENGTH_KEY} = {PIN_DIGITS}")
diff --git a/tools/tests/test_gen_fixture_wrap.py b/tools/tests/test_gen_fixture_wrap.py
index c7b11d5..cc8e907 100644
--- a/tools/tests/test_gen_fixture_wrap.py
+++ b/tools/tests/test_gen_fixture_wrap.py
@@ -33,6 +33,14 @@ def test_generated_wrap_matches_the_fixture() -> None:
assert gen_fixture_wrap.MASTER_KEY.hex() == fixture_constant("FIXTURE_MASTER_KEY")
+@pytest.mark.skipif(shutil.which("openssl") is None, reason="needs openssl for AES")
+def test_generated_legacy_wrap_matches_the_fixture() -> None:
+ legacy = gen_fixture_wrap.LEGACY_PIN
+ assert gen_fixture_wrap.fixture_wrap(legacy).hex() == fixture_constant("FIXTURE_LEGACY_WRAP")
+ assert legacy.decode() == fixture_constant("FIXTURE_LEGACY_PIN")
+ assert len(legacy) > gen_fixture_wrap.PIN_DIGITS # a legacy length: shows the extra dots
+
+
def test_fixture_pin_has_the_new_pin_length() -> None:
assert gen_fixture_wrap.PIN.decode() == fixture_constant("FIXTURE_PIN")
assert len(gen_fixture_wrap.PIN) == gen_fixture_wrap.PIN_DIGITS
diff --git a/tools/tests/test_ui_flow.py b/tools/tests/test_ui_flow.py
index d98b4c2..5e359c2 100644
--- a/tools/tests/test_ui_flow.py
+++ b/tools/tests/test_ui_flow.py
@@ -81,3 +81,13 @@ def test_barcodes_on_reads_a_drawn_code(tmp_path: Path) -> None:
screen = Image.open(path).convert("RGB")
assert ui_flow.barcodes_on(screen) == ["OTPW stress"]
assert ui_flow.barcodes_on(Image.new("RGB", (100, 100), "white")) == []
+
+
+def test_vertical_shift_measures_a_scrolled_list() -> None:
+ rows = Image.new("L", (60, 400), 0)
+ for top in range(0, 400, 50): # bands of different shades, like menu items
+ rows.paste(40 + top // 2, (5, top + 5, 55, top + 40))
+ before = rows.crop((0, 0, 60, 200))
+ assert ui_flow.vertical_shift(before, rows.crop((0, 37, 60, 237))) == 37
+ assert ui_flow.vertical_shift(rows.crop((0, 37, 60, 237)), before) == -37
+ assert ui_flow.vertical_shift(before, before) == 0
diff --git a/tools/ui_flow.py b/tools/ui_flow.py
index 3c7afb2..3796e85 100644
--- a/tools/ui_flow.py
+++ b/tools/ui_flow.py
@@ -6,7 +6,8 @@
Usage (inside the dev container, with the simulator running and the
UI-test build launched by docker/scripts/screenshots.sh):
ui_flow.py --device fr70 --device-dir DIR --log APP.log --window ID
- --out build/screenshots/fr70 --golden test/golden/fr70 [--approve]
+ --out build/screenshots/fr70 --golden test/golden/fr70
+ [--scenario main|extra] [--approve]
Input goes to the simulator window with xdotool: clicks on the device
drawing's buttons (positions from DIR/simulator.json) and taps and swipes on
@@ -29,10 +30,31 @@ list, a TOTP entry, a HOTP entry and its next code, the
PIN screen, a wrong PIN, a correct PIN opening a locked entry, auto-lock
closing it, the menu, the About screen (watches with buttons; touch-only
menus have no focus to move), the lockout after 5 wrong PINs, the barcodes
-whose screenshots must decode with zxing-cpp (README 12.4). The glance with
-the favourite's code is a separate launch (--shot glance_code, from
-screenshots.sh): the simulator doesn't return to the glance when the app
-exits.
+whose screenshots must decode with zxing-cpp (README 12.4), the Steam and
+Library entries, and the text screens the menu opens (settings results,
+entry IDs, clock status, About on touch-only watches, the benchmark),
+scrolled with the buttons or a swipe. The glance with the favourite's code
+is a separate launch (--shot glance_code, from screenshots.sh): the
+simulator doesn't return to the glance when the app exits.
+
+--scenario extra drives the second UI-test build (uitest-extra.jungle,
+Fixtures.loadExtra): a "Require PIN" from the Manage form waiting while the
+watch is locked; UPC-A and Code 39 cards that must decode; an EAN-13 with a
+wrong check digit (the code error screen); a QR whose encoding progress must
+start low and only go up; a 6-digit PIN from before the 4-digit rule,
+unlocked with OK and replaced (new PIN, a repeat that differs, the repeat);
+the waiting lock applied at that PIN; auto-lock closing a locked code;
+Change PIN; Reset PIN and creating a PIN. Its glance (no favourite code) is
+checked as glance.png.
+
+Menus are reached by label: the debug build logs the menu's items ("DIAG
+menu items=..."). Watches with buttons move the focus; touch-only watches
+(Venu Sq 2, Venu X1) have none, so the item is tapped where that watch's
+menu draws it (TOUCH_MENU_ROWS), after slow drags whose distance is
+measured on the screen.
+
+Screens whose content changes from run to run (the encoding progress, the
+benchmark's timings) are saved for review only, never compared.
Each screenshot is compared with test/golden/<device>/<screen>.png like
sim_screenshot.py does (or saved there with --approve).
@@ -43,6 +65,7 @@ Exit: 0 all screens match, 1 a screen differs or is missing a golden image,
from __future__ import annotations
import argparse
+import itertools
import json
import re
import subprocess
@@ -53,7 +76,7 @@ from pathlib import Path
import sim_screenshot as shots
import zxingcpp
-from PIL import Image
+from PIL import Image, ImageChops, ImageStat
EXIT_OK, EXIT_DIFFERENT, EXIT_FLOW = 0, 1, 2
SCREEN_TIMEOUT_S = 20.0
@@ -80,6 +103,27 @@ LOCKER_CODE128 = "FAKE-0042" # source/app/Fixtures.mc: the Add code form's Code
# The Add OTP form's TOTP ("Hello!" demo key) at TestMode.NOW is 309848, next
# 489290 (RFC 6238 computed in Python); the golden image otp_form shows them.
QR_TIMEOUT_S = 30.0 # the QR is encoded in timer steps the first time
+LEGACY_PIN = "123456" # source/app/Fixtures.mc FIXTURE_LEGACY_PIN: a PIN from before the 4-digit rule
+NEW_PIN, OTHER_PIN, CHANGED_PIN, CREATED_PIN = "5678", "5679", "2468", "1357"
+SELECTOR_LEGACY = 11 # the selector adds "OK" (position 10) for a legacy PIN
+KEY_OK = -2 # source/ui/PinView.mc: the keypad's OK key (legacy PINs only)
+MODE_UNLOCK, MODE_CREATE, MODE_CHANGE = 0, 1, 2 # source/ui/PinView.mc
+# source/app/Fixtures.mc loadExtra: the UPC-A 036000291452, which zxing-cpp
+# reports as the EAN-13 it is a subset of, and the Code 39 "FAKE-39" with the
+# mod 43 check character the app adds ("L"), which zxing-cpp reports as text.
+SHOP_UPCA = "0036000291452"
+BADGE_CODE39 = "FAKE-39L"
+BIG_QR_TEXT = "Fake event ticket 0042 for the UI test." + " Row 12, seat 34, gate b, valid all day." * 4
+PROGRESS_FIRST_MAX = 30 # the encoding progress must start below this (percent)
+PROGRESS_MIN_STEPS = 3 # and show at least this many different values
+BENCH_TIMEOUT_S = 30.0
+# Touch-only menus (no focus): the centre of the first item and the item
+# pitch, in screen pixels, at the top of a Menu2 with a title, as these
+# watches draw it (measured from the simulator).
+TOUCH_MENU_ROWS = {"venusq2": (100, 100), "venux1": (184, 140)}
+DRAG_STEPS = 30 # a slow drag: the list follows the finger without a fling
+DRAG_HOLD_S = 0.6
+MAX_MENU_DRAGS = 16
class FlowError(Exception):
@@ -99,6 +143,22 @@ def barcodes_on(screen: Image.Image) -> list[str]:
return [r.text for r in zxingcpp.read_barcodes(screen)]
+def vertical_shift(before: Image.Image, after: Image.Image) -> int:
+ """How many pixels the content moved up from `before` to `after` (a
+ scrolled list; negative: down): the shift whose overlapping rows differ
+ least, compared in the middle half of the screen."""
+ a, b = before.convert("L"), after.convert("L")
+ width, height = a.size
+ best, best_score = 0, float("inf")
+ top, bottom = height // 4, height * 3 // 4
+ for shift in range(-(height // 4), height // 4 + 1):
+ moved = ImageChops.difference(a.crop((0, top + shift, width, bottom + shift)), b.crop((0, top, width, bottom)))
+ score = ImageStat.Stat(moved).mean[0]
+ if score < best_score:
+ best, best_score = shift, score
+ return best
+
+
def load_keys(device_dir: Path) -> dict[str, Key]:
"""Button centres in device-drawing coordinates, by button id (enter, up, down, esc, menu)."""
sim = json.loads((device_dir / "simulator.json").read_text(encoding="utf-8"))
@@ -215,6 +275,18 @@ class Simulator:
self._xdotool("mouseup", "1")
time.sleep(CLICK_PAUSE_S)
+ def drag(self, sx: int, sy0: int, sy1: int) -> None:
+ """A slow vertical drag that stops before lifting the finger, so a
+ list moves with it and doesn't fling on."""
+ self._move(*self._to_window(self.display.x + sx, self.display.y + sy0))
+ self._xdotool("mousedown", "1")
+ for i in range(1, DRAG_STEPS + 1):
+ self._move(*self._to_window(self.display.x + sx, self.display.y + sy0 + (sy1 - sy0) * i // DRAG_STEPS))
+ time.sleep(0.03)
+ time.sleep(DRAG_HOLD_S)
+ self._xdotool("mouseup", "1")
+ time.sleep(CLICK_PAUSE_S)
+
def swipe(self, sx0: int, sy0: int, sx1: int, sy1: int) -> None:
self._move(*self._to_window(self.display.x + sx0, self.display.y + sy0))
self._xdotool("mousedown", "1")
@@ -288,7 +360,11 @@ class Flow:
else:
self.sim.tap(self._w() // 2, self._h() // 2)
- def enter_pin(self, pin: str) -> None:
+ def enter_pin(self, pin: str, legacy: bool = False, ok: bool | None = None) -> None:
+ """Type a PIN; the 4th digit submits it. A legacy PIN (any length)
+ is submitted with OK (unless ok is False: more digits follow), after
+ which the screen's selector is back at 0."""
+ submit = legacy if ok is None else ok
if self.touch:
keys = self.log.latest(r"DIAG pin_keys (\S+)")
if keys is None:
@@ -298,15 +374,101 @@ class Flow:
value, xy = item.split("=")
x, y = xy.split(",")
centres[int(value)] = (int(x), int(y))
- for digit in pin: # the 4th digit submits
+ for digit in pin:
self.sim.tap(*centres[int(digit)])
+ if submit:
+ if KEY_OK not in centres:
+ raise FlowError("the legacy PIN keypad has no OK key")
+ self.sim.tap(*centres[KEY_OK])
return
- for position in [int(d) for d in pin]:
- steps = (position - self.selector) % SELECTOR_DIGITS
+ positions = SELECTOR_LEGACY if legacy else SELECTOR_DIGITS
+ for position in [int(d) for d in pin] + ([SELECTOR_DIGITS] if submit else []):
+ steps = (position - self.selector) % positions
for _ in range(steps):
self.sim.press("down")
self.selector = position
self.sim.press("enter")
+ if submit:
+ self.selector = 0 # the new-PIN screen has no OK position
+
+ def expect_pin(self, mode: int, digits: int = 0, repeat: bool = False, busy: bool = False) -> None:
+ """Wait until the PIN screen shows this state (PinView MODE_*: 0
+ unlock, 1 create, 2 change): every key press redraws the screen, so
+ "shown pin" alone can come before the last digit arrived."""
+ state = f"mode={mode} digits={digits} repeat={int(repeat)} busy={int(busy)}"
+ self.log.wait_for(rf"DIAG pin_state {state}\s*$", SCREEN_TIMEOUT_S)
+ time.sleep(SETTLE_S)
+
+ def menu_items(self) -> list[str]:
+ """The open menu's item labels, from its debug log line."""
+ items = self.log.latest(r"DIAG menu items=(.*?)\s*$")
+ if items is None:
+ raise FlowError("the menu didn't report its items")
+ return items.group(1).split("|")
+
+ def open_menu_item(self, label: str) -> None:
+ """Open the menu and select the item `label`."""
+ self.act()
+ self.menu()
+ self.log.wait_for(r"DIAG menu items=", SCREEN_TIMEOUT_S)
+ time.sleep(MENU_SETTLE_S)
+ items = self.menu_items()
+ if label not in items:
+ raise FlowError(f"the menu has no item {label!r}: {items}")
+ index = items.index(label)
+ self.act()
+ if self.sim.has("down"):
+ for _ in range(index):
+ self.next()
+ time.sleep(MENU_SETTLE_S)
+ self.select()
+ return
+ self.tap_touch_menu_item(index)
+
+ def tap_touch_menu_item(self, index: int) -> None:
+ """Tap item `index` of a menu without focus, dragging it into view
+ first; each drag's real distance is measured on the screen."""
+ name = self.sim.device_dir.name
+ if name not in TOUCH_MENU_ROWS:
+ raise FlowError(f"no menu layout for the touch-only watch {name} in TOUCH_MENU_ROWS")
+ first, pitch = TOUCH_MENU_ROWS[name]
+ x, h = self._w() // 2, self._h()
+ # A new menu doesn't always start at the top: drag down to it first.
+ for _ in range(MAX_MENU_DRAGS):
+ before = self.screen("menu-before")
+ self.sim.drag(x, h // 4, h // 4 + h // 5)
+ time.sleep(MENU_SETTLE_S)
+ if vertical_shift(before, self.screen("menu-after")) == 0:
+ break
+ y = first + index * pitch
+ for _ in range(MAX_MENU_DRAGS):
+ if y <= h - pitch // 2:
+ break
+ before = self.screen("menu-before")
+ distance = min(y - h // 2, h // 5)
+ self.sim.drag(x, h * 3 // 4, h * 3 // 4 - distance)
+ time.sleep(MENU_SETTLE_S)
+ shift = vertical_shift(before, self.screen("menu-after"))
+ if shift <= 0:
+ break # the end of the menu
+ y -= shift
+ if not 0 < y < h:
+ raise FlowError(f"menu item {index} can't be brought into view")
+ self.sim.tap(x, y)
+
+ def screen(self, name: str) -> Image.Image:
+ """The current screen, uncompared (saved under raw/)."""
+ return shots.extract_screen(self.sim.capture(name), self.sim.device_dir, self.out / "raw" / f"{name}.png")
+
+ def mark_after(self, pattern: str, timeout: float = SCREEN_TIMEOUT_S) -> None:
+ """Wait for a line matching `pattern`; later waits look only after it."""
+ self.log.wait_for(pattern, timeout)
+ regex = re.compile(pattern)
+ lines = self.log.lines()
+ for index in range(len(lines) - 1, self.log.mark - 1, -1):
+ if regex.search(lines[index]):
+ self.log.mark = index + 1
+ return
# -- checking -----------------------------------------------------------
@@ -322,7 +484,9 @@ class Flow:
"""Call before an input whose result `expect` should wait for."""
self.log.set_mark()
- def shot(self, name: str, expect_code: str | None = None) -> None:
+ def shot(self, name: str, expect_code: str | None = None, review: bool = False) -> None:
+ """Screenshot as `name` and compare it with its golden image; with
+ review, only save it (screens that differ from run to run)."""
raw = self.sim.capture(name)
out = self.out / f"{name}.png"
label = f"{self.sim.device_dir.name}/{name}"
@@ -332,6 +496,9 @@ class Flow:
print(f"[{label}] {err}")
self.results[name] = EXIT_DIFFERENT
return
+ if review:
+ print(f"[{label}] saved for review only: {out}")
+ return
if expect_code is not None:
# README 12.4 check 3: the code on the screenshot must decode.
read = barcodes_on(screen)
@@ -562,6 +729,60 @@ def flow_code(f: Flow) -> None:
f.shot(name, expect_code=expected)
+def flow_details(f: Flow) -> None:
+ """From the Aztec card (index 10): the Steam (4) and Library (5) entries."""
+ f.act()
+ f.back()
+ f.expect("list")
+ for _ in range(6):
+ f.prev()
+ for name in ("otp_steam", "otp_library"):
+ f.act()
+ f.open_focused_row()
+ f.expect("otp")
+ f.shot(name)
+ f.act()
+ f.back()
+ f.expect("list")
+ f.next()
+
+
+def flow_info_screens(f: Flow) -> None:
+ """The text screens the menu opens, each scrolled with a swipe on touch
+ screens; the entry IDs (110 entries) also one page down. Touch-only
+ watches get About here (the others in flow_menu). The benchmark's
+ timings vary, so its screens are kept for review only."""
+ screens = [
+ ("Settings results", "results", "menu_results"),
+ ("Entry IDs", "ids", "entry_ids"),
+ ("Clock status", "clock", "clock_status"),
+ ]
+ if not f.sim.has("down"):
+ screens.append(("About", "about", "about"))
+ for label, screen, name in screens:
+ f.open_menu_item(label)
+ f.expect(screen)
+ f.shot(name)
+ flow_swipes(f, screen)
+ if screen == "ids":
+ f.act()
+ f.next()
+ f.log.wait_for(r"DIAG info_scroll first=[1-9]", SCREEN_TIMEOUT_S)
+ time.sleep(SETTLE_S)
+ f.shot("entry_ids_next")
+ f.act()
+ f.back()
+ f.expect("list")
+ f.open_menu_item("Debug: benchmark")
+ f.expect("bench_running")
+ f.shot("bench_running", review=True)
+ f.expect("bench", BENCH_TIMEOUT_S)
+ f.shot("bench", review=True)
+ f.act()
+ f.back()
+ f.expect("list")
+
+
def run_flow(f: Flow) -> None:
"""The scripted session; see the module docstring."""
flow_import(f)
@@ -572,6 +793,177 @@ def run_flow(f: Flow) -> None:
flow_queue(f)
flow_lockout(f)
flow_code(f)
+ flow_details(f)
+ flow_info_screens(f)
+
+
+def open_row(f: Flow, moves: int, screen: str, timeout: float = SCREEN_TIMEOUT_S) -> None:
+ """Move the list focus by `moves` rows (negative: up) and open that row."""
+ for _ in range(abs(moves)):
+ if moves > 0:
+ f.next()
+ else:
+ f.prev()
+ f.act()
+ f.open_focused_row()
+ f.expect(screen, timeout)
+
+
+def close_to_list(f: Flow) -> None:
+ f.act()
+ f.back()
+ f.expect("list")
+
+
+def extra_codes(f: Flow) -> None:
+ """From GitHub (0): UPC-A (2) and Code 39 (3) must decode, the EAN-13
+ with a wrong check digit (4) shows the code error, and the large QR (5)
+ shows its encoding progress, which must start low and only go up."""
+ open_row(f, 2, "code")
+ f.shot("code_upca", expect_code=SHOP_UPCA)
+ close_to_list(f)
+ open_row(f, 1, "code")
+ f.shot("code_39", expect_code=BADGE_CODE39)
+ close_to_list(f)
+ open_row(f, 1, "code_error")
+ f.shot("code_error")
+ close_to_list(f)
+ f.next()
+ f.act()
+ f.open_focused_row()
+ f.log.wait_for(r"DIAG code_progress percent=", SCREEN_TIMEOUT_S)
+ f.shot("code_progress", review=True)
+ f.expect("code", QR_TIMEOUT_S)
+ check_progress(f)
+ f.shot("code_qr_large", expect_code=BIG_QR_TEXT)
+ close_to_list(f)
+
+
+def check_progress(f: Flow) -> None:
+ """The encoding progress since the last mark starts low, never goes
+ back, and moves through several values."""
+ regex = re.compile(r"DIAG code_progress percent=(\d+)")
+ seen = [int(m.group(1)) for line in f.log.lines()[f.log.mark :] if (m := regex.search(line))]
+ steps = sorted(set(seen))
+ print(f"[{f.sim.device_dir.name}] encoding progress: {' '.join(str(p) for p in steps)} %")
+ if not seen or seen[0] >= PROGRESS_FIRST_MAX:
+ raise FlowError(f"the encoding progress started at {seen[:1]}, not below {PROGRESS_FIRST_MAX} %")
+ if any(b < a for a, b in itertools.pairwise(seen)):
+ raise FlowError(f"the encoding progress went back: {seen}")
+ if len(steps) < PROGRESS_MIN_STEPS:
+ raise FlowError(f"the encoding progress showed only {steps}")
+
+
+def extra_legacy_pin(f: Flow) -> None:
+ """From the large QR (5): Bank (1) asks for the legacy 6-digit PIN with
+ OK; it unlocks, applies the waiting "Require PIN", then asks for a new
+ 4-digit PIN: a repeat that differs, then the same PIN twice opens Bank."""
+ open_row(f, -4, "pin")
+ f.opened_pin_screen()
+ f.shot("pin_legacy")
+ f.act()
+ f.enter_pin(LEGACY_PIN[:-1], legacy=True, ok=False)
+ f.expect_pin(MODE_UNLOCK, digits=len(LEGACY_PIN) - 1)
+ f.shot("pin_legacy_typed")
+ f.act()
+ f.enter_pin(LEGACY_PIN[-1], legacy=True)
+ f.log.wait_for(r"DIAG locks_applied count=1\b", SCREEN_TIMEOUT_S)
+ f.expect_pin(MODE_CHANGE)
+ f.shot("pin_new_forced")
+ f.act()
+ f.enter_pin(NEW_PIN)
+ f.expect_pin(MODE_CHANGE, repeat=True)
+ f.shot("pin_repeat")
+ f.act()
+ f.enter_pin(OTHER_PIN)
+ f.expect("pin_error")
+ f.shot("pin_differ")
+ f.act()
+ f.enter_pin(NEW_PIN)
+ f.expect_pin(MODE_CHANGE, repeat=True)
+ f.act()
+ f.enter_pin(NEW_PIN)
+ f.expect("otp")
+ close_to_list(f)
+ f.shot("list_locked")
+
+
+def extra_auto_lock_code(f: Flow) -> None:
+ """The Shop card (2) now requires the PIN; the session is unlocked, so
+ it opens directly, and auto-lock closes it back to the list."""
+ open_row(f, 1, "code")
+ f.act()
+ f.expect("list", AUTO_LOCK_TIMEOUT_S)
+
+
+def extra_change_pin(f: Flow) -> None:
+ """Locked: Change PIN asks for the current PIN, then a new one twice.
+ After "Lock now", the new PIN opens Bank (1)."""
+ f.open_menu_item("Change PIN")
+ f.expect_pin(MODE_UNLOCK)
+ f.opened_pin_screen()
+ f.act()
+ f.enter_pin(NEW_PIN)
+ f.expect_pin(MODE_CHANGE)
+ f.opened_pin_screen()
+ f.shot("pin_change")
+ f.act()
+ f.enter_pin(CHANGED_PIN)
+ f.expect_pin(MODE_CHANGE, repeat=True)
+ f.act()
+ f.enter_pin(CHANGED_PIN)
+ f.expect("list")
+ f.open_menu_item("Lock now") # changing the PIN left the watch unlocked
+ f.expect("list")
+ open_row(f, -1, "pin")
+ f.opened_pin_screen()
+ f.act()
+ f.enter_pin(CHANGED_PIN)
+ f.expect("otp")
+ close_to_list(f)
+
+
+def extra_reset_pin(f: Flow) -> None:
+ """Reset PIN: the confirmation, then creating a new PIN (entered twice)."""
+ f.open_menu_item("Reset PIN")
+ time.sleep(MENU_SETTLE_S)
+ f.shot("reset_confirm")
+ f.act()
+ confirm(f)
+ f.expect_pin(MODE_CREATE)
+ f.opened_pin_screen()
+ f.shot("pin_create")
+ f.act()
+ f.enter_pin(CREATED_PIN)
+ f.expect_pin(MODE_CREATE, repeat=True)
+ f.act()
+ f.enter_pin(CREATED_PIN)
+ f.expect("list")
+ f.shot("list_reset")
+
+
+def confirm(f: Flow) -> None:
+ """Answer yes in a native confirmation: UP to "Yes" and START with
+ buttons, a tap on the right half ("Confirm") on touch-only watches."""
+ if f.sim.has("up"):
+ f.sim.press("up")
+ time.sleep(MENU_SETTLE_S)
+ f.sim.press("enter")
+ else:
+ f.sim.tap(f._w() * 3 // 4, f._h() * 5 // 8)
+
+
+def run_extra_flow(f: Flow) -> None:
+ """The extra scenario's session; see the module docstring."""
+ f.expect("results", IMPORT_TIMEOUT_S) # the Manage form's "Require PIN" waits for the PIN
+ f.shot("lock_queued")
+ close_to_list(f)
+ f.shot("list_extra")
+ extra_codes(f)
+ extra_legacy_pin(f)
+ extra_auto_lock_code(f)
+ extra_change_pin(f)
+ extra_reset_pin(f)
def find_key(data: object, key: str) -> object:
@@ -655,6 +1047,7 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--log", type=Path, help="the app's console log (monkeydo output)")
parser.add_argument("--golden", type=Path, help="directory of the approved images")
parser.add_argument("--approve", action="store_true")
+ parser.add_argument("--scenario", choices=["main", "extra"], default="main", help="which UI-test build runs")
parser.add_argument("--open-from-glance", action="store_true", help="only open the app from its glance")
parser.add_argument("--decode", metavar="TEXT", help="only check that the screen shows a barcode reading TEXT")
parser.add_argument("--shot", metavar="NAME", help="only screenshot the current screen as NAME (needs --golden)")
@@ -673,7 +1066,7 @@ def main(argv: list[str] | None = None) -> int:
try:
sim = Simulator(args.window, args.device_dir, args.out / "raw")
flow = Flow(sim, AppLog(args.log), args.out, args.golden, args.approve)
- run_flow(flow)
+ (run_extra_flow if args.scenario == "extra" else run_flow)(flow)
except (FlowError, subprocess.CalledProcessError, OSError) as err:
print(f"[{name}] UI flow failed: {err}")
return EXIT_FLOW
diff --git a/uitest-extra.jungle b/uitest-extra.jungle
new file mode 100644
index 0000000..96e8266
--- /dev/null
+++ b/uitest-extra.jungle
@@ -0,0 +1,8 @@
+# Author: cirakg & claude ai
+# Summary: Second UI-test build: the extra scenario's fixture (legacy PIN, queued "Require PIN", more symbologies).
+#
+# Used on top of monkey.jungle by `build-app.sh uitest-extra` (make
+# screenshots): like uitest.jungle, but it keeps the (:uitest_extra) version
+# of source/util/TestMode.mc instead of the (:uitest_main) one, so
+# source/app/Fixtures.mc loads the extra scenario. Never shipped.
+base.excludeAnnotations = no_uitest;stress;uitest_main
diff --git a/uitest.jungle b/uitest.jungle
index eab75b8..6988112 100644
--- a/uitest.jungle
+++ b/uitest.jungle
@@ -4,5 +4,7 @@
# Used on top of monkey.jungle by `build-app.sh uitest` (make screenshots):
# it keeps the (:uitest) code and drops the (:no_uitest) alternatives, so the
# app starts with a reset store full of fake entries and a frozen clock
-# (source/util/TestMode.mc). Never shipped: release builds use monkey.jungle.
-base.excludeAnnotations = no_uitest;stress
+# (source/util/TestMode.mc). It drops the (:uitest_extra) scenario, which
+# uitest-extra.jungle keeps instead. Never shipped: release builds use
+# monkey.jungle.
+base.excludeAnnotations = no_uitest;stress;uitest_extra