fix(sample-app): address PR #18 review findings (#19)

- WebLedgerStore.accountExistsByName case-insensitive (matches SQL COLLATE NOCASE)
- drop SampleApplication.maybeInjectDebugError; sample spec now passes clean
- drop noLogcatErrors from sample spec (default matches system-wide E logs)
- openRandomAccount picks uniformly from findAll instead of first match
- clear txnError on any add-transaction interaction, not only valid amount input
- escapeForAdbInputText quotes shell metacharacters (quotes, backslash, etc.)
- extract buildClearKeyevents helper with empty/normal/cap tests
- broaden spec_integration_test.go to cover home, add-account, ledger,
  add-transaction action generators
- document FocusTracker single-focus invariant
This commit is contained in:
pj authored and GitHub committed 2026-04-20 13:31:50 +07:00
1 parent 8381a98aaf
commit c1de4bf57a
8 files changed
+284 -26

No files matched your search

@@ -1,7 +1,6 @@
package dev.uatu.sample
import android.app.Application
import android.content.pm.ApplicationInfo
import dev.uatu.sdk.Uatu
class SampleApplication : Application() {
@@ -80,16 +79,5 @@ class SampleApplication : Application() {
Uatu.extract("login_error") { UiState.loginError.value }
Uatu.extract("add_account_error") { UiState.addAccountError.value }
Uatu.extract("txn_error") { UiState.txnError.value }
maybeInjectDebugError()
}
// Fires a synthetic Uatu.reportError in debug builds so the sample-app
// e2e run can verify noUncaughtExceptions surfaces SDK-captured errors
// in the trace. Production builds skip this.
private fun maybeInjectDebugError() {
val isDebuggable = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
if (isDebuggable) {
Uatu.reportError(RuntimeException("synthetic"))
}
}
}
@@ -4,6 +4,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
// Assumes single-focus: only one input can be focused at a time, so
// enter() overwriting is safe. leave() is id-gated so a stale dispose
// from a previously-focused field cannot clobber the active focus.
object FocusTracker {
private val _current = MutableStateFlow<String?>(null)
val current: StateFlow<String?> = _current.asStateFlow()
@@ -103,7 +103,10 @@ fun AddTransactionPage(accountId: String) {
Segmented(
selected = if (type == TxnType.credit) 0 else 1,
labels = listOf("Credit", "Debit"),
onSelect = { type = if (it == 0) TxnType.credit else TxnType.debit },
onSelect = {
UiState.txnError.value = ""
type = if (it == 0) TxnType.credit else TxnType.debit
},
descriptions = listOf("txn_credit", "txn_debit"),
)
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
@@ -111,9 +114,9 @@ fun AddTransactionPage(accountId: String) {
TextInput(
value = amount,
onChange = {
UiState.txnError.value = ""
if (AMOUNT_REGEX.matches(it) || it.isEmpty()) {
amount = it
UiState.txnError.value = ""
}
},
placeholder = "0.00",
@@ -129,7 +132,10 @@ fun AddTransactionPage(accountId: String) {
FieldLabel("Note (optional)")
TextInput(
value = note,
onChange = { note = it.take(80) },
onChange = {
UiState.txnError.value = ""
note = it.take(80)
},
placeholder = "What's this for?",
label = "Note",
description = "txn_note",
@@ -21,7 +21,7 @@ class WebLedgerStore : LedgerStore {
}
override fun accountExistsByName(name: String): Boolean =
_accounts.value.any { it.name == name }
_accounts.value.any { it.name.equals(name, ignoreCase = true) }
override fun insertAccount(id: String, name: String, createdAt: Long) {
_accounts.value = _accounts.value + Account(id, name, createdAt)
+8 -5
View File
@@ -14,7 +14,7 @@ import {
waitOnce,
weighted,
} from "@uatu/spec";
import { noLogcatErrors, noUncaughtExceptions } from "@uatu/spec/defaults/properties";
import { noUncaughtExceptions } from "@uatu/spec/defaults/properties";
interface AccountSnapshot {
id: string;
@@ -90,7 +90,9 @@ const txnCredit = extract((state) => state.ax.find("desc:txn_credit"));
const txnDebit = extract((state) => state.ax.find("desc:txn_debit"));
const txnSubmit = extract((state) => state.ax.find("desc:txn_submit"));
const backButton = extract((state) => state.ax.find("desc:Back"));
const anyAccountCard = extract((state) => state.ax.find("descPrefix:account_card:"));
const allAccountCards = extract((state) =>
state.ax.findAll("descPrefix:account_card:"),
);
const accountCountNonNegative = always(() => accountCount.current >= 0);
@@ -334,8 +336,10 @@ const openAddAccount = actions(() => {
const openRandomAccount = actions(() => {
if (route.current !== "home") return [];
const card = anyAccountCard.current;
return card ? [Tap({ on: card })] : [];
const cards = allAccountCards.current;
if (cards.length === 0) return [];
const card = cards[Math.floor(Math.random() * cards.length)];
return [Tap({ on: card })];
});
const logoutAction = actions(() => {
@@ -411,7 +415,6 @@ export const properties = {
...stateMachine,
...liveness,
noUncaughtExceptions,
noLogcatErrors,
};
export const actionsRoot = weighted(