diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-25 23:51:52 +0100 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-25 23:51:52 +0100 |
| commit | b8addc87f804789368612bf8e1be31a2bf28efe1 (patch) | |
| tree | bece4051eb6f009eba05ffdd585420232741a7d7 | |
| parent | 140a6660f63f88d05ba9aae0bf1c4b401bb6574d (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>
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 Binary files differnew file mode 100644 index 0000000..8c1df0a --- /dev/null +++ b/test/golden/fenix7/clock_status.png diff --git a/test/golden/fenix7/code_39.png b/test/golden/fenix7/code_39.png Binary files differnew file mode 100644 index 0000000..8c6abaa --- /dev/null +++ b/test/golden/fenix7/code_39.png diff --git a/test/golden/fenix7/code_error.png b/test/golden/fenix7/code_error.png Binary files differnew file mode 100644 index 0000000..655d6f7 --- /dev/null +++ b/test/golden/fenix7/code_error.png diff --git a/test/golden/fenix7/code_qr_large.png b/test/golden/fenix7/code_qr_large.png Binary files differnew file mode 100644 index 0000000..ae253f3 --- /dev/null +++ b/test/golden/fenix7/code_qr_large.png diff --git a/test/golden/fenix7/code_upca.png b/test/golden/fenix7/code_upca.png Binary files differnew file mode 100644 index 0000000..33a281d --- /dev/null +++ b/test/golden/fenix7/code_upca.png diff --git a/test/golden/fenix7/entry_ids.png b/test/golden/fenix7/entry_ids.png Binary files differnew file mode 100644 index 0000000..4844c12 --- /dev/null +++ b/test/golden/fenix7/entry_ids.png diff --git a/test/golden/fenix7/entry_ids_next.png b/test/golden/fenix7/entry_ids_next.png Binary files differnew file mode 100644 index 0000000..9089891 --- /dev/null +++ b/test/golden/fenix7/entry_ids_next.png diff --git a/test/golden/fenix7/glance.png b/test/golden/fenix7/glance.png Binary files differnew file mode 100644 index 0000000..912a848 --- /dev/null +++ b/test/golden/fenix7/glance.png diff --git a/test/golden/fenix7/list_extra.png b/test/golden/fenix7/list_extra.png Binary files differnew file mode 100644 index 0000000..b13f652 --- /dev/null +++ b/test/golden/fenix7/list_extra.png diff --git a/test/golden/fenix7/list_locked.png b/test/golden/fenix7/list_locked.png Binary files differnew file mode 100644 index 0000000..ec0aafd --- /dev/null +++ b/test/golden/fenix7/list_locked.png diff --git a/test/golden/fenix7/list_reset.png b/test/golden/fenix7/list_reset.png Binary files differnew file mode 100644 index 0000000..e9e2e8a --- /dev/null +++ b/test/golden/fenix7/list_reset.png diff --git a/test/golden/fenix7/lock_queued.png b/test/golden/fenix7/lock_queued.png Binary files differnew file mode 100644 index 0000000..2affaa0 --- /dev/null +++ b/test/golden/fenix7/lock_queued.png diff --git a/test/golden/fenix7/menu_results.png b/test/golden/fenix7/menu_results.png Binary files differnew file mode 100644 index 0000000..5608acf --- /dev/null +++ b/test/golden/fenix7/menu_results.png diff --git a/test/golden/fenix7/otp_library.png b/test/golden/fenix7/otp_library.png Binary files differnew file mode 100644 index 0000000..b2a588f --- /dev/null +++ b/test/golden/fenix7/otp_library.png diff --git a/test/golden/fenix7/otp_steam.png b/test/golden/fenix7/otp_steam.png Binary files differnew file mode 100644 index 0000000..a972361 --- /dev/null +++ b/test/golden/fenix7/otp_steam.png diff --git a/test/golden/fenix7/pin.png b/test/golden/fenix7/pin.png Binary files differindex d1fe80b..5b093dc 100644 --- a/test/golden/fenix7/pin.png +++ b/test/golden/fenix7/pin.png diff --git a/test/golden/fenix7/pin_change.png b/test/golden/fenix7/pin_change.png Binary files differnew file mode 100644 index 0000000..25cf42f --- /dev/null +++ b/test/golden/fenix7/pin_change.png diff --git a/test/golden/fenix7/pin_create.png b/test/golden/fenix7/pin_create.png Binary files differnew file mode 100644 index 0000000..236155c --- /dev/null +++ b/test/golden/fenix7/pin_create.png diff --git a/test/golden/fenix7/pin_differ.png b/test/golden/fenix7/pin_differ.png Binary files differnew file mode 100644 index 0000000..f738e6c --- /dev/null +++ b/test/golden/fenix7/pin_differ.png diff --git a/test/golden/fenix7/pin_error.png b/test/golden/fenix7/pin_error.png Binary files differindex 48541c8..c3fa1dc 100644 --- a/test/golden/fenix7/pin_error.png +++ b/test/golden/fenix7/pin_error.png diff --git a/test/golden/fenix7/pin_legacy.png b/test/golden/fenix7/pin_legacy.png Binary files differnew file mode 100644 index 0000000..01286ea --- /dev/null +++ b/test/golden/fenix7/pin_legacy.png diff --git a/test/golden/fenix7/pin_legacy_typed.png b/test/golden/fenix7/pin_legacy_typed.png Binary files differnew file mode 100644 index 0000000..7c0b1d7 --- /dev/null +++ b/test/golden/fenix7/pin_legacy_typed.png diff --git a/test/golden/fenix7/pin_new_forced.png b/test/golden/fenix7/pin_new_forced.png Binary files differnew file mode 100644 index 0000000..9ad113c --- /dev/null +++ b/test/golden/fenix7/pin_new_forced.png diff --git a/test/golden/fenix7/pin_repeat.png b/test/golden/fenix7/pin_repeat.png Binary files differnew file mode 100644 index 0000000..8095fc8 --- /dev/null +++ b/test/golden/fenix7/pin_repeat.png diff --git a/test/golden/fenix7/pin_wait.png b/test/golden/fenix7/pin_wait.png Binary files differindex 77cce98..c856f5c 100644 --- a/test/golden/fenix7/pin_wait.png +++ b/test/golden/fenix7/pin_wait.png diff --git a/test/golden/fenix7/reset_confirm.png b/test/golden/fenix7/reset_confirm.png Binary files differnew file mode 100644 index 0000000..36b8044 --- /dev/null +++ b/test/golden/fenix7/reset_confirm.png diff --git a/test/golden/fr255s/clock_status.png b/test/golden/fr255s/clock_status.png Binary files differnew file mode 100644 index 0000000..7c853b4 --- /dev/null +++ b/test/golden/fr255s/clock_status.png diff --git a/test/golden/fr255s/code_39.png b/test/golden/fr255s/code_39.png Binary files differnew file mode 100644 index 0000000..f566d67 --- /dev/null +++ b/test/golden/fr255s/code_39.png diff --git a/test/golden/fr255s/code_error.png b/test/golden/fr255s/code_error.png Binary files differnew file mode 100644 index 0000000..de0805f --- /dev/null +++ b/test/golden/fr255s/code_error.png diff --git a/test/golden/fr255s/code_qr_large.png b/test/golden/fr255s/code_qr_large.png Binary files differnew file mode 100644 index 0000000..cf9f93f --- /dev/null +++ b/test/golden/fr255s/code_qr_large.png diff --git a/test/golden/fr255s/code_upca.png b/test/golden/fr255s/code_upca.png Binary files differnew file mode 100644 index 0000000..79aa52c --- /dev/null +++ b/test/golden/fr255s/code_upca.png diff --git a/test/golden/fr255s/entry_ids.png b/test/golden/fr255s/entry_ids.png Binary files differnew file mode 100644 index 0000000..56aef73 --- /dev/null +++ b/test/golden/fr255s/entry_ids.png diff --git a/test/golden/fr255s/entry_ids_next.png b/test/golden/fr255s/entry_ids_next.png Binary files differnew file mode 100644 index 0000000..46ce47b --- /dev/null +++ b/test/golden/fr255s/entry_ids_next.png diff --git a/test/golden/fr255s/glance.png b/test/golden/fr255s/glance.png Binary files differnew file mode 100644 index 0000000..df0dde1 --- /dev/null +++ b/test/golden/fr255s/glance.png diff --git a/test/golden/fr255s/list_extra.png b/test/golden/fr255s/list_extra.png Binary files differnew file mode 100644 index 0000000..109ad21 --- /dev/null +++ b/test/golden/fr255s/list_extra.png diff --git a/test/golden/fr255s/list_locked.png b/test/golden/fr255s/list_locked.png Binary files differnew file mode 100644 index 0000000..88624d1 --- /dev/null +++ b/test/golden/fr255s/list_locked.png diff --git a/test/golden/fr255s/list_reset.png b/test/golden/fr255s/list_reset.png Binary files differnew file mode 100644 index 0000000..66b10c6 --- /dev/null +++ b/test/golden/fr255s/list_reset.png diff --git a/test/golden/fr255s/lock_queued.png b/test/golden/fr255s/lock_queued.png Binary files differnew file mode 100644 index 0000000..f274a9b --- /dev/null +++ b/test/golden/fr255s/lock_queued.png diff --git a/test/golden/fr255s/menu_results.png b/test/golden/fr255s/menu_results.png Binary files differnew file mode 100644 index 0000000..96326d1 --- /dev/null +++ b/test/golden/fr255s/menu_results.png diff --git a/test/golden/fr255s/otp_library.png b/test/golden/fr255s/otp_library.png Binary files differnew file mode 100644 index 0000000..4126205 --- /dev/null +++ b/test/golden/fr255s/otp_library.png diff --git a/test/golden/fr255s/otp_steam.png b/test/golden/fr255s/otp_steam.png Binary files differnew file mode 100644 index 0000000..d4e4fe0 --- /dev/null +++ b/test/golden/fr255s/otp_steam.png diff --git a/test/golden/fr255s/pin.png b/test/golden/fr255s/pin.png Binary files differindex bb3e989..d388b78 100644 --- a/test/golden/fr255s/pin.png +++ b/test/golden/fr255s/pin.png diff --git a/test/golden/fr255s/pin_change.png b/test/golden/fr255s/pin_change.png Binary files differnew file mode 100644 index 0000000..1cfa707 --- /dev/null +++ b/test/golden/fr255s/pin_change.png diff --git a/test/golden/fr255s/pin_create.png b/test/golden/fr255s/pin_create.png Binary files differnew file mode 100644 index 0000000..b89194e --- /dev/null +++ b/test/golden/fr255s/pin_create.png diff --git a/test/golden/fr255s/pin_differ.png b/test/golden/fr255s/pin_differ.png Binary files differnew file mode 100644 index 0000000..0e3de3a --- /dev/null +++ b/test/golden/fr255s/pin_differ.png diff --git a/test/golden/fr255s/pin_error.png b/test/golden/fr255s/pin_error.png Binary files differindex b580338..320220e 100644 --- a/test/golden/fr255s/pin_error.png +++ b/test/golden/fr255s/pin_error.png diff --git a/test/golden/fr255s/pin_legacy.png b/test/golden/fr255s/pin_legacy.png Binary files differnew file mode 100644 index 0000000..f7ee814 --- /dev/null +++ b/test/golden/fr255s/pin_legacy.png diff --git a/test/golden/fr255s/pin_legacy_typed.png b/test/golden/fr255s/pin_legacy_typed.png Binary files differnew file mode 100644 index 0000000..84cbb8f --- /dev/null +++ b/test/golden/fr255s/pin_legacy_typed.png diff --git a/test/golden/fr255s/pin_new_forced.png b/test/golden/fr255s/pin_new_forced.png Binary files differnew file mode 100644 index 0000000..06d1974 --- /dev/null +++ b/test/golden/fr255s/pin_new_forced.png diff --git a/test/golden/fr255s/pin_repeat.png b/test/golden/fr255s/pin_repeat.png Binary files differnew file mode 100644 index 0000000..7e69048 --- /dev/null +++ b/test/golden/fr255s/pin_repeat.png diff --git a/test/golden/fr255s/pin_wait.png b/test/golden/fr255s/pin_wait.png Binary files differindex cc1e27b..b584db2 100644 --- a/test/golden/fr255s/pin_wait.png +++ b/test/golden/fr255s/pin_wait.png diff --git a/test/golden/fr255s/reset_confirm.png b/test/golden/fr255s/reset_confirm.png Binary files differnew file mode 100644 index 0000000..f90cfc4 --- /dev/null +++ b/test/golden/fr255s/reset_confirm.png diff --git a/test/golden/fr70/about.png b/test/golden/fr70/about.png Binary files differindex c92b268..7636e45 100644 --- a/test/golden/fr70/about.png +++ b/test/golden/fr70/about.png diff --git a/test/golden/fr70/clock_status.png b/test/golden/fr70/clock_status.png Binary files differnew file mode 100644 index 0000000..bac3d93 --- /dev/null +++ b/test/golden/fr70/clock_status.png diff --git a/test/golden/fr70/code_39.png b/test/golden/fr70/code_39.png Binary files differnew file mode 100644 index 0000000..5b12d54 --- /dev/null +++ b/test/golden/fr70/code_39.png diff --git a/test/golden/fr70/code_error.png b/test/golden/fr70/code_error.png Binary files differnew file mode 100644 index 0000000..77255c9 --- /dev/null +++ b/test/golden/fr70/code_error.png diff --git a/test/golden/fr70/code_qr_large.png b/test/golden/fr70/code_qr_large.png Binary files differnew file mode 100644 index 0000000..635e19c --- /dev/null +++ b/test/golden/fr70/code_qr_large.png diff --git a/test/golden/fr70/code_upca.png b/test/golden/fr70/code_upca.png Binary files differnew file mode 100644 index 0000000..a08c3b5 --- /dev/null +++ b/test/golden/fr70/code_upca.png diff --git a/test/golden/fr70/entry_ids.png b/test/golden/fr70/entry_ids.png Binary files differnew file mode 100644 index 0000000..c021f2a --- /dev/null +++ b/test/golden/fr70/entry_ids.png diff --git a/test/golden/fr70/entry_ids_next.png b/test/golden/fr70/entry_ids_next.png Binary files differnew file mode 100644 index 0000000..c61c6cd --- /dev/null +++ b/test/golden/fr70/entry_ids_next.png diff --git a/test/golden/fr70/glance.png b/test/golden/fr70/glance.png Binary files differnew file mode 100644 index 0000000..7d56b74 --- /dev/null +++ b/test/golden/fr70/glance.png diff --git a/test/golden/fr70/list_extra.png b/test/golden/fr70/list_extra.png Binary files differnew file mode 100644 index 0000000..207a7de --- /dev/null +++ b/test/golden/fr70/list_extra.png diff --git a/test/golden/fr70/list_locked.png b/test/golden/fr70/list_locked.png Binary files differnew file mode 100644 index 0000000..2981ae9 --- /dev/null +++ b/test/golden/fr70/list_locked.png diff --git a/test/golden/fr70/list_reset.png b/test/golden/fr70/list_reset.png Binary files differnew file mode 100644 index 0000000..7b6f369 --- /dev/null +++ b/test/golden/fr70/list_reset.png diff --git a/test/golden/fr70/lock_queued.png b/test/golden/fr70/lock_queued.png Binary files differnew file mode 100644 index 0000000..b81677b --- /dev/null +++ b/test/golden/fr70/lock_queued.png diff --git a/test/golden/fr70/menu_results.png b/test/golden/fr70/menu_results.png Binary files differnew file mode 100644 index 0000000..eb9cb8c --- /dev/null +++ b/test/golden/fr70/menu_results.png diff --git a/test/golden/fr70/otp_library.png b/test/golden/fr70/otp_library.png Binary files differnew file mode 100644 index 0000000..50c4dcc --- /dev/null +++ b/test/golden/fr70/otp_library.png diff --git a/test/golden/fr70/otp_steam.png b/test/golden/fr70/otp_steam.png Binary files differnew file mode 100644 index 0000000..6a90090 --- /dev/null +++ b/test/golden/fr70/otp_steam.png diff --git a/test/golden/fr70/pin.png b/test/golden/fr70/pin.png Binary files differindex 71f6579..1c7863e 100644 --- a/test/golden/fr70/pin.png +++ b/test/golden/fr70/pin.png diff --git a/test/golden/fr70/pin_change.png b/test/golden/fr70/pin_change.png Binary files differnew file mode 100644 index 0000000..e52dec5 --- /dev/null +++ b/test/golden/fr70/pin_change.png diff --git a/test/golden/fr70/pin_create.png b/test/golden/fr70/pin_create.png Binary files differnew file mode 100644 index 0000000..96dc179 --- /dev/null +++ b/test/golden/fr70/pin_create.png diff --git a/test/golden/fr70/pin_differ.png b/test/golden/fr70/pin_differ.png Binary files differnew file mode 100644 index 0000000..a8966ea --- /dev/null +++ b/test/golden/fr70/pin_differ.png diff --git a/test/golden/fr70/pin_error.png b/test/golden/fr70/pin_error.png Binary files differindex 9a3de60..aab9982 100644 --- a/test/golden/fr70/pin_error.png +++ b/test/golden/fr70/pin_error.png diff --git a/test/golden/fr70/pin_legacy.png b/test/golden/fr70/pin_legacy.png Binary files differnew file mode 100644 index 0000000..d103e86 --- /dev/null +++ b/test/golden/fr70/pin_legacy.png diff --git a/test/golden/fr70/pin_legacy_typed.png b/test/golden/fr70/pin_legacy_typed.png Binary files differnew file mode 100644 index 0000000..d8e7038 --- /dev/null +++ b/test/golden/fr70/pin_legacy_typed.png diff --git a/test/golden/fr70/pin_new_forced.png b/test/golden/fr70/pin_new_forced.png Binary files differnew file mode 100644 index 0000000..ced0c5b --- /dev/null +++ b/test/golden/fr70/pin_new_forced.png diff --git a/test/golden/fr70/pin_repeat.png b/test/golden/fr70/pin_repeat.png Binary files differnew file mode 100644 index 0000000..574bda6 --- /dev/null +++ b/test/golden/fr70/pin_repeat.png diff --git a/test/golden/fr70/pin_wait.png b/test/golden/fr70/pin_wait.png Binary files differindex ed8132e..9b726f8 100644 --- a/test/golden/fr70/pin_wait.png +++ b/test/golden/fr70/pin_wait.png diff --git a/test/golden/fr70/reset_confirm.png b/test/golden/fr70/reset_confirm.png Binary files differnew file mode 100644 index 0000000..56351e9 --- /dev/null +++ b/test/golden/fr70/reset_confirm.png diff --git a/test/golden/fr965/clock_status.png b/test/golden/fr965/clock_status.png Binary files differnew file mode 100644 index 0000000..2b0a7c2 --- /dev/null +++ b/test/golden/fr965/clock_status.png diff --git a/test/golden/fr965/code_39.png b/test/golden/fr965/code_39.png Binary files differnew file mode 100644 index 0000000..fd62d3e --- /dev/null +++ b/test/golden/fr965/code_39.png diff --git a/test/golden/fr965/code_error.png b/test/golden/fr965/code_error.png Binary files differnew file mode 100644 index 0000000..6601baf --- /dev/null +++ b/test/golden/fr965/code_error.png diff --git a/test/golden/fr965/code_qr_large.png b/test/golden/fr965/code_qr_large.png Binary files differnew file mode 100644 index 0000000..69fea25 --- /dev/null +++ b/test/golden/fr965/code_qr_large.png diff --git a/test/golden/fr965/code_upca.png b/test/golden/fr965/code_upca.png Binary files differnew file mode 100644 index 0000000..45b4ac5 --- /dev/null +++ b/test/golden/fr965/code_upca.png diff --git a/test/golden/fr965/entry_ids.png b/test/golden/fr965/entry_ids.png Binary files differnew file mode 100644 index 0000000..cbd911f --- /dev/null +++ b/test/golden/fr965/entry_ids.png diff --git a/test/golden/fr965/entry_ids_next.png b/test/golden/fr965/entry_ids_next.png Binary files differnew file mode 100644 index 0000000..0bb5206 --- /dev/null +++ b/test/golden/fr965/entry_ids_next.png diff --git a/test/golden/fr965/glance.png b/test/golden/fr965/glance.png Binary files differnew file mode 100644 index 0000000..8aa9eca --- /dev/null +++ b/test/golden/fr965/glance.png diff --git a/test/golden/fr965/list_extra.png b/test/golden/fr965/list_extra.png Binary files differnew file mode 100644 index 0000000..cdee2ed --- /dev/null +++ b/test/golden/fr965/list_extra.png diff --git a/test/golden/fr965/list_locked.png b/test/golden/fr965/list_locked.png Binary files differnew file mode 100644 index 0000000..55fb2c1 --- /dev/null +++ b/test/golden/fr965/list_locked.png diff --git a/test/golden/fr965/list_reset.png b/test/golden/fr965/list_reset.png Binary files differnew file mode 100644 index 0000000..8df05f9 --- /dev/null +++ b/test/golden/fr965/list_reset.png diff --git a/test/golden/fr965/lock_queued.png b/test/golden/fr965/lock_queued.png Binary files differnew file mode 100644 index 0000000..8cdd66b --- /dev/null +++ b/test/golden/fr965/lock_queued.png diff --git a/test/golden/fr965/menu_results.png b/test/golden/fr965/menu_results.png Binary files differnew file mode 100644 index 0000000..1781e6e --- /dev/null +++ b/test/golden/fr965/menu_results.png diff --git a/test/golden/fr965/otp_library.png b/test/golden/fr965/otp_library.png Binary files differnew file mode 100644 index 0000000..d19ecb3 --- /dev/null +++ b/test/golden/fr965/otp_library.png diff --git a/test/golden/fr965/otp_steam.png b/test/golden/fr965/otp_steam.png Binary files differnew file mode 100644 index 0000000..3bb4d79 --- /dev/null +++ b/test/golden/fr965/otp_steam.png diff --git a/test/golden/fr965/pin.png b/test/golden/fr965/pin.png Binary files differindex 06cab65..a85643e 100644 --- a/test/golden/fr965/pin.png +++ b/test/golden/fr965/pin.png diff --git a/test/golden/fr965/pin_change.png b/test/golden/fr965/pin_change.png Binary files differnew file mode 100644 index 0000000..2beee26 --- /dev/null +++ b/test/golden/fr965/pin_change.png diff --git a/test/golden/fr965/pin_create.png b/test/golden/fr965/pin_create.png Binary files differnew file mode 100644 index 0000000..808253b --- /dev/null +++ b/test/golden/fr965/pin_create.png diff --git a/test/golden/fr965/pin_differ.png b/test/golden/fr965/pin_differ.png Binary files differnew file mode 100644 index 0000000..50b4fa1 --- /dev/null +++ b/test/golden/fr965/pin_differ.png diff --git a/test/golden/fr965/pin_error.png b/test/golden/fr965/pin_error.png Binary files differindex c187ada..319cf13 100644 --- a/test/golden/fr965/pin_error.png +++ b/test/golden/fr965/pin_error.png diff --git a/test/golden/fr965/pin_legacy.png b/test/golden/fr965/pin_legacy.png Binary files differnew file mode 100644 index 0000000..c3ecd06 --- /dev/null +++ b/test/golden/fr965/pin_legacy.png diff --git a/test/golden/fr965/pin_legacy_typed.png b/test/golden/fr965/pin_legacy_typed.png Binary files differnew file mode 100644 index 0000000..788feaf --- /dev/null +++ b/test/golden/fr965/pin_legacy_typed.png diff --git a/test/golden/fr965/pin_new_forced.png b/test/golden/fr965/pin_new_forced.png Binary files differnew file mode 100644 index 0000000..e83c73f --- /dev/null +++ b/test/golden/fr965/pin_new_forced.png diff --git a/test/golden/fr965/pin_repeat.png b/test/golden/fr965/pin_repeat.png Binary files differnew file mode 100644 index 0000000..9a60945 --- /dev/null +++ b/test/golden/fr965/pin_repeat.png diff --git a/test/golden/fr965/pin_wait.png b/test/golden/fr965/pin_wait.png Binary files differindex e381d23..24c2a33 100644 --- a/test/golden/fr965/pin_wait.png +++ b/test/golden/fr965/pin_wait.png diff --git a/test/golden/fr965/reset_confirm.png b/test/golden/fr965/reset_confirm.png Binary files differnew file mode 100644 index 0000000..faa5fd1 --- /dev/null +++ b/test/golden/fr965/reset_confirm.png diff --git a/test/golden/instinct3amoled45mm/clock_status.png b/test/golden/instinct3amoled45mm/clock_status.png Binary files differnew file mode 100644 index 0000000..d7dd009 --- /dev/null +++ b/test/golden/instinct3amoled45mm/clock_status.png diff --git a/test/golden/instinct3amoled45mm/code_39.png b/test/golden/instinct3amoled45mm/code_39.png Binary files differnew file mode 100644 index 0000000..4403ad8 --- /dev/null +++ b/test/golden/instinct3amoled45mm/code_39.png diff --git a/test/golden/instinct3amoled45mm/code_error.png b/test/golden/instinct3amoled45mm/code_error.png Binary files differnew file mode 100644 index 0000000..a4f9ee8 --- /dev/null +++ b/test/golden/instinct3amoled45mm/code_error.png diff --git a/test/golden/instinct3amoled45mm/code_qr_large.png b/test/golden/instinct3amoled45mm/code_qr_large.png Binary files differnew file mode 100644 index 0000000..ddc19da --- /dev/null +++ b/test/golden/instinct3amoled45mm/code_qr_large.png diff --git a/test/golden/instinct3amoled45mm/code_upca.png b/test/golden/instinct3amoled45mm/code_upca.png Binary files differnew file mode 100644 index 0000000..7ee5625 --- /dev/null +++ b/test/golden/instinct3amoled45mm/code_upca.png diff --git a/test/golden/instinct3amoled45mm/entry_ids.png b/test/golden/instinct3amoled45mm/entry_ids.png Binary files differnew file mode 100644 index 0000000..17b6fc6 --- /dev/null +++ b/test/golden/instinct3amoled45mm/entry_ids.png diff --git a/test/golden/instinct3amoled45mm/entry_ids_next.png b/test/golden/instinct3amoled45mm/entry_ids_next.png Binary files differnew file mode 100644 index 0000000..be04703 --- /dev/null +++ b/test/golden/instinct3amoled45mm/entry_ids_next.png diff --git a/test/golden/instinct3amoled45mm/glance.png b/test/golden/instinct3amoled45mm/glance.png Binary files differnew file mode 100644 index 0000000..94fcfb9 --- /dev/null +++ b/test/golden/instinct3amoled45mm/glance.png diff --git a/test/golden/instinct3amoled45mm/list_extra.png b/test/golden/instinct3amoled45mm/list_extra.png Binary files differnew file mode 100644 index 0000000..a7d2ca9 --- /dev/null +++ b/test/golden/instinct3amoled45mm/list_extra.png diff --git a/test/golden/instinct3amoled45mm/list_locked.png b/test/golden/instinct3amoled45mm/list_locked.png Binary files differnew file mode 100644 index 0000000..3a6a6e1 --- /dev/null +++ b/test/golden/instinct3amoled45mm/list_locked.png diff --git a/test/golden/instinct3amoled45mm/list_reset.png b/test/golden/instinct3amoled45mm/list_reset.png Binary files differnew file mode 100644 index 0000000..19030d3 --- /dev/null +++ b/test/golden/instinct3amoled45mm/list_reset.png diff --git a/test/golden/instinct3amoled45mm/lock_queued.png b/test/golden/instinct3amoled45mm/lock_queued.png Binary files differnew file mode 100644 index 0000000..4fbdcb1 --- /dev/null +++ b/test/golden/instinct3amoled45mm/lock_queued.png diff --git a/test/golden/instinct3amoled45mm/menu_results.png b/test/golden/instinct3amoled45mm/menu_results.png Binary files differnew file mode 100644 index 0000000..2f5da34 --- /dev/null +++ b/test/golden/instinct3amoled45mm/menu_results.png diff --git a/test/golden/instinct3amoled45mm/otp_library.png b/test/golden/instinct3amoled45mm/otp_library.png Binary files differnew file mode 100644 index 0000000..5896364 --- /dev/null +++ b/test/golden/instinct3amoled45mm/otp_library.png diff --git a/test/golden/instinct3amoled45mm/otp_steam.png b/test/golden/instinct3amoled45mm/otp_steam.png Binary files differnew file mode 100644 index 0000000..9f21ae0 --- /dev/null +++ b/test/golden/instinct3amoled45mm/otp_steam.png diff --git a/test/golden/instinct3amoled45mm/pin.png b/test/golden/instinct3amoled45mm/pin.png Binary files differindex 92b60d5..79cad73 100644 --- a/test/golden/instinct3amoled45mm/pin.png +++ b/test/golden/instinct3amoled45mm/pin.png diff --git a/test/golden/instinct3amoled45mm/pin_change.png b/test/golden/instinct3amoled45mm/pin_change.png Binary files differnew file mode 100644 index 0000000..8d5f742 --- /dev/null +++ b/test/golden/instinct3amoled45mm/pin_change.png diff --git a/test/golden/instinct3amoled45mm/pin_create.png b/test/golden/instinct3amoled45mm/pin_create.png Binary files differnew file mode 100644 index 0000000..1010339 --- /dev/null +++ b/test/golden/instinct3amoled45mm/pin_create.png diff --git a/test/golden/instinct3amoled45mm/pin_differ.png b/test/golden/instinct3amoled45mm/pin_differ.png Binary files differnew file mode 100644 index 0000000..26969d8 --- /dev/null +++ b/test/golden/instinct3amoled45mm/pin_differ.png diff --git a/test/golden/instinct3amoled45mm/pin_error.png b/test/golden/instinct3amoled45mm/pin_error.png Binary files differindex 9d330b8..2c8a922 100644 --- a/test/golden/instinct3amoled45mm/pin_error.png +++ b/test/golden/instinct3amoled45mm/pin_error.png diff --git a/test/golden/instinct3amoled45mm/pin_legacy.png b/test/golden/instinct3amoled45mm/pin_legacy.png Binary files differnew file mode 100644 index 0000000..5619fd9 --- /dev/null +++ b/test/golden/instinct3amoled45mm/pin_legacy.png diff --git a/test/golden/instinct3amoled45mm/pin_legacy_typed.png b/test/golden/instinct3amoled45mm/pin_legacy_typed.png Binary files differnew file mode 100644 index 0000000..d127e7e --- /dev/null +++ b/test/golden/instinct3amoled45mm/pin_legacy_typed.png diff --git a/test/golden/instinct3amoled45mm/pin_new_forced.png b/test/golden/instinct3amoled45mm/pin_new_forced.png Binary files differnew file mode 100644 index 0000000..7f8b91e --- /dev/null +++ b/test/golden/instinct3amoled45mm/pin_new_forced.png diff --git a/test/golden/instinct3amoled45mm/pin_repeat.png b/test/golden/instinct3amoled45mm/pin_repeat.png Binary files differnew file mode 100644 index 0000000..8c2fb9a --- /dev/null +++ b/test/golden/instinct3amoled45mm/pin_repeat.png diff --git a/test/golden/instinct3amoled45mm/pin_wait.png b/test/golden/instinct3amoled45mm/pin_wait.png Binary files differindex 39eae8d..a6d2141 100644 --- a/test/golden/instinct3amoled45mm/pin_wait.png +++ b/test/golden/instinct3amoled45mm/pin_wait.png diff --git a/test/golden/instinct3amoled45mm/reset_confirm.png b/test/golden/instinct3amoled45mm/reset_confirm.png Binary files differnew file mode 100644 index 0000000..38cf4da --- /dev/null +++ b/test/golden/instinct3amoled45mm/reset_confirm.png diff --git a/test/golden/venusq2/about.png b/test/golden/venusq2/about.png Binary files differnew file mode 100644 index 0000000..14f3947 --- /dev/null +++ b/test/golden/venusq2/about.png diff --git a/test/golden/venusq2/clock_status.png b/test/golden/venusq2/clock_status.png Binary files differnew file mode 100644 index 0000000..bb374a4 --- /dev/null +++ b/test/golden/venusq2/clock_status.png diff --git a/test/golden/venusq2/code_39.png b/test/golden/venusq2/code_39.png Binary files differnew file mode 100644 index 0000000..70ee369 --- /dev/null +++ b/test/golden/venusq2/code_39.png diff --git a/test/golden/venusq2/code_error.png b/test/golden/venusq2/code_error.png Binary files differnew file mode 100644 index 0000000..13ffbbe --- /dev/null +++ b/test/golden/venusq2/code_error.png diff --git a/test/golden/venusq2/code_qr_large.png b/test/golden/venusq2/code_qr_large.png Binary files differnew file mode 100644 index 0000000..24a0101 --- /dev/null +++ b/test/golden/venusq2/code_qr_large.png diff --git a/test/golden/venusq2/code_upca.png b/test/golden/venusq2/code_upca.png Binary files differnew file mode 100644 index 0000000..1371573 --- /dev/null +++ b/test/golden/venusq2/code_upca.png diff --git a/test/golden/venusq2/entry_ids.png b/test/golden/venusq2/entry_ids.png Binary files differnew file mode 100644 index 0000000..db31b32 --- /dev/null +++ b/test/golden/venusq2/entry_ids.png diff --git a/test/golden/venusq2/entry_ids_next.png b/test/golden/venusq2/entry_ids_next.png Binary files differnew file mode 100644 index 0000000..f667d36 --- /dev/null +++ b/test/golden/venusq2/entry_ids_next.png diff --git a/test/golden/venusq2/glance.png b/test/golden/venusq2/glance.png Binary files differnew file mode 100644 index 0000000..fd0f0de --- /dev/null +++ b/test/golden/venusq2/glance.png diff --git a/test/golden/venusq2/import_results.png b/test/golden/venusq2/import_results.png Binary files differindex d908572..9182911 100644 --- a/test/golden/venusq2/import_results.png +++ b/test/golden/venusq2/import_results.png diff --git a/test/golden/venusq2/list_extra.png b/test/golden/venusq2/list_extra.png Binary files differnew file mode 100644 index 0000000..6c905ca --- /dev/null +++ b/test/golden/venusq2/list_extra.png diff --git a/test/golden/venusq2/list_locked.png b/test/golden/venusq2/list_locked.png Binary files differnew file mode 100644 index 0000000..2463f99 --- /dev/null +++ b/test/golden/venusq2/list_locked.png diff --git a/test/golden/venusq2/list_reset.png b/test/golden/venusq2/list_reset.png Binary files differnew file mode 100644 index 0000000..755bb20 --- /dev/null +++ b/test/golden/venusq2/list_reset.png diff --git a/test/golden/venusq2/lock_queued.png b/test/golden/venusq2/lock_queued.png Binary files differnew file mode 100644 index 0000000..b72824a --- /dev/null +++ b/test/golden/venusq2/lock_queued.png diff --git a/test/golden/venusq2/menu_results.png b/test/golden/venusq2/menu_results.png Binary files differnew file mode 100644 index 0000000..714cbd5 --- /dev/null +++ b/test/golden/venusq2/menu_results.png diff --git a/test/golden/venusq2/otp_library.png b/test/golden/venusq2/otp_library.png Binary files differnew file mode 100644 index 0000000..84e7972 --- /dev/null +++ b/test/golden/venusq2/otp_library.png diff --git a/test/golden/venusq2/otp_steam.png b/test/golden/venusq2/otp_steam.png Binary files differnew file mode 100644 index 0000000..6cc6c02 --- /dev/null +++ b/test/golden/venusq2/otp_steam.png diff --git a/test/golden/venusq2/pin.png b/test/golden/venusq2/pin.png Binary files differindex 9b51943..ef526ff 100644 --- a/test/golden/venusq2/pin.png +++ b/test/golden/venusq2/pin.png diff --git a/test/golden/venusq2/pin_change.png b/test/golden/venusq2/pin_change.png Binary files differnew file mode 100644 index 0000000..795d24d --- /dev/null +++ b/test/golden/venusq2/pin_change.png diff --git a/test/golden/venusq2/pin_create.png b/test/golden/venusq2/pin_create.png Binary files differnew file mode 100644 index 0000000..d1d55b2 --- /dev/null +++ b/test/golden/venusq2/pin_create.png diff --git a/test/golden/venusq2/pin_differ.png b/test/golden/venusq2/pin_differ.png Binary files differnew file mode 100644 index 0000000..3a79ecb --- /dev/null +++ b/test/golden/venusq2/pin_differ.png diff --git a/test/golden/venusq2/pin_error.png b/test/golden/venusq2/pin_error.png Binary files differindex 80a803e..ec58981 100644 --- a/test/golden/venusq2/pin_error.png +++ b/test/golden/venusq2/pin_error.png diff --git a/test/golden/venusq2/pin_legacy.png b/test/golden/venusq2/pin_legacy.png Binary files differnew file mode 100644 index 0000000..49a57b1 --- /dev/null +++ b/test/golden/venusq2/pin_legacy.png diff --git a/test/golden/venusq2/pin_legacy_typed.png b/test/golden/venusq2/pin_legacy_typed.png Binary files differnew file mode 100644 index 0000000..e9f3096 --- /dev/null +++ b/test/golden/venusq2/pin_legacy_typed.png diff --git a/test/golden/venusq2/pin_new_forced.png b/test/golden/venusq2/pin_new_forced.png Binary files differnew file mode 100644 index 0000000..4951be0 --- /dev/null +++ b/test/golden/venusq2/pin_new_forced.png diff --git a/test/golden/venusq2/pin_repeat.png b/test/golden/venusq2/pin_repeat.png Binary files differnew file mode 100644 index 0000000..d87c7f1 --- /dev/null +++ b/test/golden/venusq2/pin_repeat.png diff --git a/test/golden/venusq2/pin_wait.png b/test/golden/venusq2/pin_wait.png Binary files differindex 618e517..58a4f27 100644 --- a/test/golden/venusq2/pin_wait.png +++ b/test/golden/venusq2/pin_wait.png diff --git a/test/golden/venusq2/reset_confirm.png b/test/golden/venusq2/reset_confirm.png Binary files differnew file mode 100644 index 0000000..ec0dba2 --- /dev/null +++ b/test/golden/venusq2/reset_confirm.png diff --git a/test/golden/venux1/about.png b/test/golden/venux1/about.png Binary files differnew file mode 100644 index 0000000..0f0cca3 --- /dev/null +++ b/test/golden/venux1/about.png diff --git a/test/golden/venux1/clock_status.png b/test/golden/venux1/clock_status.png Binary files differnew file mode 100644 index 0000000..20347e7 --- /dev/null +++ b/test/golden/venux1/clock_status.png diff --git a/test/golden/venux1/code_39.png b/test/golden/venux1/code_39.png Binary files differnew file mode 100644 index 0000000..0e13911 --- /dev/null +++ b/test/golden/venux1/code_39.png diff --git a/test/golden/venux1/code_error.png b/test/golden/venux1/code_error.png Binary files differnew file mode 100644 index 0000000..fc90fa8 --- /dev/null +++ b/test/golden/venux1/code_error.png diff --git a/test/golden/venux1/code_qr_large.png b/test/golden/venux1/code_qr_large.png Binary files differnew file mode 100644 index 0000000..e024b46 --- /dev/null +++ b/test/golden/venux1/code_qr_large.png diff --git a/test/golden/venux1/code_upca.png b/test/golden/venux1/code_upca.png Binary files differnew file mode 100644 index 0000000..1a2c8f9 --- /dev/null +++ b/test/golden/venux1/code_upca.png diff --git a/test/golden/venux1/entry_ids.png b/test/golden/venux1/entry_ids.png Binary files differnew file mode 100644 index 0000000..9d7baae --- /dev/null +++ b/test/golden/venux1/entry_ids.png diff --git a/test/golden/venux1/entry_ids_next.png b/test/golden/venux1/entry_ids_next.png Binary files differnew file mode 100644 index 0000000..f3f237d --- /dev/null +++ b/test/golden/venux1/entry_ids_next.png diff --git a/test/golden/venux1/glance.png b/test/golden/venux1/glance.png Binary files differnew file mode 100644 index 0000000..8f81071 --- /dev/null +++ b/test/golden/venux1/glance.png diff --git a/test/golden/venux1/import_results.png b/test/golden/venux1/import_results.png Binary files differindex 3f27b17..faeca2d 100644 --- a/test/golden/venux1/import_results.png +++ b/test/golden/venux1/import_results.png diff --git a/test/golden/venux1/list_extra.png b/test/golden/venux1/list_extra.png Binary files differnew file mode 100644 index 0000000..793192d --- /dev/null +++ b/test/golden/venux1/list_extra.png diff --git a/test/golden/venux1/list_locked.png b/test/golden/venux1/list_locked.png Binary files differnew file mode 100644 index 0000000..6842136 --- /dev/null +++ b/test/golden/venux1/list_locked.png diff --git a/test/golden/venux1/list_reset.png b/test/golden/venux1/list_reset.png Binary files differnew file mode 100644 index 0000000..8ec9498 --- /dev/null +++ b/test/golden/venux1/list_reset.png diff --git a/test/golden/venux1/lock_queued.png b/test/golden/venux1/lock_queued.png Binary files differnew file mode 100644 index 0000000..e9717f4 --- /dev/null +++ b/test/golden/venux1/lock_queued.png diff --git a/test/golden/venux1/menu_results.png b/test/golden/venux1/menu_results.png Binary files differnew file mode 100644 index 0000000..8512577 --- /dev/null +++ b/test/golden/venux1/menu_results.png diff --git a/test/golden/venux1/otp_library.png b/test/golden/venux1/otp_library.png Binary files differnew file mode 100644 index 0000000..4707b71 --- /dev/null +++ b/test/golden/venux1/otp_library.png diff --git a/test/golden/venux1/otp_steam.png b/test/golden/venux1/otp_steam.png Binary files differnew file mode 100644 index 0000000..3810a3a --- /dev/null +++ b/test/golden/venux1/otp_steam.png diff --git a/test/golden/venux1/pin.png b/test/golden/venux1/pin.png Binary files differindex 6263938..9100cc4 100644 --- a/test/golden/venux1/pin.png +++ b/test/golden/venux1/pin.png diff --git a/test/golden/venux1/pin_change.png b/test/golden/venux1/pin_change.png Binary files differnew file mode 100644 index 0000000..ae8ff2b --- /dev/null +++ b/test/golden/venux1/pin_change.png diff --git a/test/golden/venux1/pin_create.png b/test/golden/venux1/pin_create.png Binary files differnew file mode 100644 index 0000000..e29e072 --- /dev/null +++ b/test/golden/venux1/pin_create.png diff --git a/test/golden/venux1/pin_differ.png b/test/golden/venux1/pin_differ.png Binary files differnew file mode 100644 index 0000000..69f32e4 --- /dev/null +++ b/test/golden/venux1/pin_differ.png diff --git a/test/golden/venux1/pin_error.png b/test/golden/venux1/pin_error.png Binary files differindex ff2e723..c0a9029 100644 --- a/test/golden/venux1/pin_error.png +++ b/test/golden/venux1/pin_error.png diff --git a/test/golden/venux1/pin_legacy.png b/test/golden/venux1/pin_legacy.png Binary files differnew file mode 100644 index 0000000..b5e7e23 --- /dev/null +++ b/test/golden/venux1/pin_legacy.png diff --git a/test/golden/venux1/pin_legacy_typed.png b/test/golden/venux1/pin_legacy_typed.png Binary files differnew file mode 100644 index 0000000..74c79ad --- /dev/null +++ b/test/golden/venux1/pin_legacy_typed.png diff --git a/test/golden/venux1/pin_new_forced.png b/test/golden/venux1/pin_new_forced.png Binary files differnew file mode 100644 index 0000000..948a890 --- /dev/null +++ b/test/golden/venux1/pin_new_forced.png diff --git a/test/golden/venux1/pin_repeat.png b/test/golden/venux1/pin_repeat.png Binary files differnew file mode 100644 index 0000000..b3b8858 --- /dev/null +++ b/test/golden/venux1/pin_repeat.png diff --git a/test/golden/venux1/pin_wait.png b/test/golden/venux1/pin_wait.png Binary files differindex c91126f..d253871 100644 --- a/test/golden/venux1/pin_wait.png +++ b/test/golden/venux1/pin_wait.png diff --git a/test/golden/venux1/reset_confirm.png b/test/golden/venux1/reset_confirm.png Binary files differnew file mode 100644 index 0000000..31c2030 --- /dev/null +++ b/test/golden/venux1/reset_confirm.png 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 |
