From 2373b6f02ef027a423762d5d6349cd9e0f36090e Mon Sep 17 00:00:00 2001 From: cirakg Date: Mon, 19 Oct 2026 22:28:09 +0200 Subject: test(ui): drive the simulator with a scripted flow and check every screen tools/ui_flow.py clicks buttons and taps from the device files, waits for DIAG shown lines and compares 10 screens per watch with goldens on four test and three spot-check devices. Adds the fixture-wrap generator, UI logic tests and driver tests; the launcher opens apps from their glance. Co-Authored-By: Claude Opus 5.5 --- test/ui/UiLogicTest.mc | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 test/ui/UiLogicTest.mc (limited to 'test/ui/UiLogicTest.mc') diff --git a/test/ui/UiLogicTest.mc b/test/ui/UiLogicTest.mc new file mode 100644 index 0000000..01b7dae --- /dev/null +++ b/test/ui/UiLogicTest.mc @@ -0,0 +1,92 @@ +// Author: cirakg & claude ai +// Summary: Tests for the UI's logic without a screen: the code cache, code grouping, text helpers and option defaults. + +import Toybox.Lang; +import Toybox.Test; + +(:test) +function codeCacheSplitsCodesForReading(logger as Test.Logger) as Boolean { + var cases = [["123456", "123|456"], ["1234567", "123|4567"], ["12345678", "1234|5678"], ["R7VXK", "R7VXK"]]; + for (var i = 0; i < cases.size(); i++) { + var c = cases[i] as Array; + var parts = CodeCache.split(c[0]); + var joined = parts.size() == 2 ? parts[0] + "|" + parts[1] : parts[0]; + if (!TestSupport.sameString(logger, "split " + c[0], joined, c[1])) { + return false; + } + } + return true; +} + +// The cache gives the same codes as the OTP module, keeps them until the +// period ends, and learns the period and cost from the encrypted part. +(:test) +function codeCacheComputesAndExpiresCodes(logger as Test.Logger) as Boolean { + var vault = new Vault(new MemoryBackend(), new FakeTime()); + var e = StorageFixtures.totp("Cache"); + var secret = Bytes.copy(e.secret as ByteArray); + var id = vault.add(e); + var cache = new CodeCache(vault); + var now = 1800000026l; // 34 s left in the fixture's 60 s period + var expected = Otp.totp(Hashes.SHA256, secret, now, 60, 8); + var next = Otp.totp(Hashes.SHA256, secret, now + 60, 60, 8); + var unknownCost = cache.costOf(e); + var code = cache.code(id, now, 0); + var stillCached = cache.cached(id, now + 33) != null; + var expired = cache.cached(id, now + 34) == null; + return TestSupport.sameString(logger, "current", code, expected) + && TestSupport.sameString(logger, "next", cache.code(id, now, 1), next) + && stillCached && expired && cache.periodOf(id) == 60 + && unknownCost == CodeCache.COST_SHA512 && cache.costOf(e) == CodeCache.COST_NATIVE; +} + +(:test) +function codeCacheHotpAndInvalidate(logger as Test.Logger) as Boolean { + var vault = new Vault(new MemoryBackend(), new FakeTime()); + var id = vault.add(StorageFixtures.hotp("Counter", 5l)); + var cache = new CodeCache(vault); + var first = cache.code(id, 0l, 0); + vault.setCounter(id, 6l); + var stale = cache.code(id, 0l, 0); + cache.invalidate(id); + var fresh = cache.code(id, 0l, 0); + // RFC 4226 Appendix D: counters 5 and 6. + return first.equals("254676") && stale.equals("254676") && fresh.equals("287922"); +} + +(:test) +function codeCacheRefusesLockedEntriesWhileLocked(logger as Test.Logger) as Boolean { + var vault = new Vault(new MemoryBackend(), new FakeTime()); + StorageFixtures.setPin(vault, StorageFixtures.PIN); + var e = StorageFixtures.totp("Locked"); + e.locked = true; + var id = vault.add(e); + var cache = new CodeCache(vault); + var unlockedCode = cache.code(id, 1800000026l, 0); + vault.lock(); + cache.clear(); + var code = 0; + try { + cache.code(id, 1800000026l, 0); + } catch (err instanceof VaultError) { + code = err.code; + } + return unlockedCode.length() == 8 && code == VaultError.LOCKED && cache.cached(id, 1800000026l) == null; +} + +(:test) +function uiTextHelpers(logger as Test.Logger) as Boolean { + var parts = Ui.split("a b c", " "); + return Ui.isDigits("123 456") && !Ui.isDigits("12a") && !Ui.isDigits("") && !Ui.isDigits("------") + && parts.size() == 4 && parts[0].equals("a") && parts[2].equals("") && parts[3].equals("c") + && Ui.minutesSeconds(90).equals("1:30") && Ui.minutesSeconds(3600).equals("60:00") && Ui.minutesSeconds(5).equals("0:05"); +} + +// resources/settings/properties.xml defaults give README 10.2's defaults. +(:test) +function optionsDefaults(logger as Test.Logger) as Boolean { + var o = new Options(); + o.load(); + return o.autoLockMs == 60000 && !o.eraseAfterFailures && o.timeOffset == 0 && o.codeSize == Options.CODE_LARGE + && o.theme == Options.THEME_DARK && o.showGroups && !o.encryptNames && !o.glanceCode && o.fullBrightness; +} -- cgit v1.2.3