From e9c2733bd63ff1ef1ecaf04e3e74fba9ce6c7e0c Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 13:27:51 -0400 Subject: [PATCH 01/19] test(maestro): fix stale appId across the suite The debug build's applicationId is `com.flipcash.app.android` (the `.dev` suffix is deprecated), but every flow still declared `com.flipcash.app.android.dev`, so none of them could launch or drive the app. Point all flows/subflows/helpers/screenshot groups at the real appId. --- maestro/claim_cashlink.yaml | 2 +- maestro/helpers/close_open_sheet.yaml | 2 +- maestro/helpers/launch_app.yaml | 2 +- maestro/helpers/launch_deeplink.yaml | 2 +- maestro/helpers/open_link_in_browser.yaml | 2 +- maestro/helpers/screenshot.yaml | 2 +- maestro/login.yaml | 2 +- maestro/login_logout.yaml | 2 +- maestro/open_token_info_deeplink.yaml | 2 +- maestro/screenshots/capture_all.yaml | 2 +- maestro/screenshots/groups/01_pre_login.yaml | 2 +- maestro/screenshots/groups/02_home.yaml | 2 +- maestro/screenshots/groups/03_menu.yaml | 2 +- maestro/screenshots/groups/04_account.yaml | 2 +- maestro/screenshots/groups/05_features.yaml | 2 +- maestro/screenshots/groups/06_give.yaml | 2 +- maestro/screenshots/groups/07_tokens.yaml | 2 +- maestro/screenshots/groups/08_withdraw.yaml | 2 +- maestro/screenshots/groups/09_history.yaml | 2 +- maestro/show_bill_and_put_back_in_wallet.yaml | 2 +- maestro/subflows/collect_own_cashlink.yaml | 2 +- maestro/subflows/login.yaml | 2 +- maestro/subflows/login_with_deeplink.yaml | 2 +- maestro/subflows/logout.yaml | 2 +- maestro/subflows/navigate_to_advanced_features.yaml | 2 +- maestro/subflows/navigate_to_app_settings.yaml | 2 +- maestro/subflows/navigate_to_give.yaml | 2 +- maestro/subflows/navigate_to_menu.yaml | 2 +- maestro/subflows/navigate_to_my_account.yaml | 2 +- maestro/subflows/navigate_to_wallet.yaml | 2 +- maestro/subflows/navigate_to_withdraw.yaml | 2 +- maestro/subflows/open_token_info.yaml | 2 +- maestro/subflows/pull_out_bill.yaml | 2 +- maestro/subflows/return_to_scanner.yaml | 2 +- maestro/subflows/share_cashlink.yaml | 2 +- maestro/view_token_info.yaml | 2 +- 36 files changed, 36 insertions(+), 36 deletions(-) diff --git a/maestro/claim_cashlink.yaml b/maestro/claim_cashlink.yaml index 4611c5bc4..0911cbdde 100644 --- a/maestro/claim_cashlink.yaml +++ b/maestro/claim_cashlink.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android onFlowStart: - runScript: scripts/extract_url.js --- diff --git a/maestro/helpers/close_open_sheet.yaml b/maestro/helpers/close_open_sheet.yaml index ef85e08d7..00c9579f5 100644 --- a/maestro/helpers/close_open_sheet.yaml +++ b/maestro/helpers/close_open_sheet.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - swipe: direction: DOWN diff --git a/maestro/helpers/launch_app.yaml b/maestro/helpers/launch_app.yaml index 55a7ff80b..5a2c929ce 100644 --- a/maestro/helpers/launch_app.yaml +++ b/maestro/helpers/launch_app.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: when: diff --git a/maestro/helpers/launch_deeplink.yaml b/maestro/helpers/launch_deeplink.yaml index f07a8198c..4260e8976 100644 --- a/maestro/helpers/launch_deeplink.yaml +++ b/maestro/helpers/launch_deeplink.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- # Conditionally clear state (launchApp + stopApp brings app out of # Android's "stopped" state so the subsequent openLink can reach it) diff --git a/maestro/helpers/open_link_in_browser.yaml b/maestro/helpers/open_link_in_browser.yaml index 36f92cf5f..cd6e48c5c 100644 --- a/maestro/helpers/open_link_in_browser.yaml +++ b/maestro/helpers/open_link_in_browser.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android # helpers/open_link_in_browser.yaml # Opens a URL from clipboard in Chrome (or another app) diff --git a/maestro/helpers/screenshot.yaml b/maestro/helpers/screenshot.yaml index 053ad157b..5f0c87211 100644 --- a/maestro/helpers/screenshot.yaml +++ b/maestro/helpers/screenshot.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - waitForAnimationToEnd - takeScreenshot: ${SCREENSHOT_NAME} diff --git a/maestro/login.yaml b/maestro/login.yaml index b2032120c..8829f04a9 100644 --- a/maestro/login.yaml +++ b/maestro/login.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: helpers/launch_app.yaml - runFlow: subflows/login.yaml diff --git a/maestro/login_logout.yaml b/maestro/login_logout.yaml index ba3b0b32c..67929ecd9 100644 --- a/maestro/login_logout.yaml +++ b/maestro/login_logout.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: helpers/launch_app.yaml - runFlow: subflows/login.yaml diff --git a/maestro/open_token_info_deeplink.yaml b/maestro/open_token_info_deeplink.yaml index e8f473d87..c47537f17 100644 --- a/maestro/open_token_info_deeplink.yaml +++ b/maestro/open_token_info_deeplink.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: subflows/login_with_deeplink.yaml - runFlow: diff --git a/maestro/screenshots/capture_all.yaml b/maestro/screenshots/capture_all.yaml index ab2462124..62d1ba586 100644 --- a/maestro/screenshots/capture_all.yaml +++ b/maestro/screenshots/capture_all.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android name: "Screenshots: All" tags: - screenshot diff --git a/maestro/screenshots/groups/01_pre_login.yaml b/maestro/screenshots/groups/01_pre_login.yaml index d2de76078..bf05db2a8 100644 --- a/maestro/screenshots/groups/01_pre_login.yaml +++ b/maestro/screenshots/groups/01_pre_login.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android name: "Screenshots: Pre-Login" tags: - screenshot diff --git a/maestro/screenshots/groups/02_home.yaml b/maestro/screenshots/groups/02_home.yaml index 0564cc35b..8bedb8445 100644 --- a/maestro/screenshots/groups/02_home.yaml +++ b/maestro/screenshots/groups/02_home.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android name: "Screenshots: Home" tags: - screenshot diff --git a/maestro/screenshots/groups/03_menu.yaml b/maestro/screenshots/groups/03_menu.yaml index 651aaf2bf..df022a19c 100644 --- a/maestro/screenshots/groups/03_menu.yaml +++ b/maestro/screenshots/groups/03_menu.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android name: "Screenshots: Menu" tags: - screenshot diff --git a/maestro/screenshots/groups/04_account.yaml b/maestro/screenshots/groups/04_account.yaml index fdeceecce..9821db58b 100644 --- a/maestro/screenshots/groups/04_account.yaml +++ b/maestro/screenshots/groups/04_account.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android name: "Screenshots: Account & Settings" tags: - screenshot diff --git a/maestro/screenshots/groups/05_features.yaml b/maestro/screenshots/groups/05_features.yaml index 7bd6ffa7d..efd835a01 100644 --- a/maestro/screenshots/groups/05_features.yaml +++ b/maestro/screenshots/groups/05_features.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android name: "Screenshots: Advanced Features" tags: - screenshot diff --git a/maestro/screenshots/groups/06_give.yaml b/maestro/screenshots/groups/06_give.yaml index ab5ec0eb9..b3e2be18e 100644 --- a/maestro/screenshots/groups/06_give.yaml +++ b/maestro/screenshots/groups/06_give.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android name: "Screenshots: Give Flow" tags: - screenshot diff --git a/maestro/screenshots/groups/07_tokens.yaml b/maestro/screenshots/groups/07_tokens.yaml index d1fa9d629..3d6262bff 100644 --- a/maestro/screenshots/groups/07_tokens.yaml +++ b/maestro/screenshots/groups/07_tokens.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android name: "Screenshots: Tokens" tags: - screenshot diff --git a/maestro/screenshots/groups/08_withdraw.yaml b/maestro/screenshots/groups/08_withdraw.yaml index 0deaac43c..c4405f4c7 100644 --- a/maestro/screenshots/groups/08_withdraw.yaml +++ b/maestro/screenshots/groups/08_withdraw.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android name: "Screenshots: Withdraw Flow" tags: - screenshot diff --git a/maestro/screenshots/groups/09_history.yaml b/maestro/screenshots/groups/09_history.yaml index 5ee8d78db..16b830a56 100644 --- a/maestro/screenshots/groups/09_history.yaml +++ b/maestro/screenshots/groups/09_history.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android name: "Screenshots: Transaction History" tags: - screenshot diff --git a/maestro/show_bill_and_put_back_in_wallet.yaml b/maestro/show_bill_and_put_back_in_wallet.yaml index 1c5bdc227..83585764d 100644 --- a/maestro/show_bill_and_put_back_in_wallet.yaml +++ b/maestro/show_bill_and_put_back_in_wallet.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: subflows/login_with_deeplink.yaml - runFlow: subflows/pull_out_bill.yaml diff --git a/maestro/subflows/collect_own_cashlink.yaml b/maestro/subflows/collect_own_cashlink.yaml index e5bd7c13c..27f7adf1f 100644 --- a/maestro/subflows/collect_own_cashlink.yaml +++ b/maestro/subflows/collect_own_cashlink.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - assertVisible: Collect - tapOn: Collect diff --git a/maestro/subflows/login.yaml b/maestro/subflows/login.yaml index f64efa948..ee347cd0b 100644 --- a/maestro/subflows/login.yaml +++ b/maestro/subflows/login.yaml @@ -1,5 +1,5 @@ # Login -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - tapOn: Log in - extendedWaitUntil: diff --git a/maestro/subflows/login_with_deeplink.yaml b/maestro/subflows/login_with_deeplink.yaml index 5391a9343..919ddb2c9 100644 --- a/maestro/subflows/login_with_deeplink.yaml +++ b/maestro/subflows/login_with_deeplink.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - stopApp - runFlow: diff --git a/maestro/subflows/logout.yaml b/maestro/subflows/logout.yaml index b502e95b4..8de8ed5e7 100644 --- a/maestro/subflows/logout.yaml +++ b/maestro/subflows/logout.yaml @@ -1,5 +1,5 @@ # Reusable Logout flow -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - tapOn: id: scanner_view diff --git a/maestro/subflows/navigate_to_advanced_features.yaml b/maestro/subflows/navigate_to_advanced_features.yaml index f664f32a9..c7ca8ea35 100644 --- a/maestro/subflows/navigate_to_advanced_features.yaml +++ b/maestro/subflows/navigate_to_advanced_features.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: navigate_to_menu.yaml - tapOn: Advanced Features diff --git a/maestro/subflows/navigate_to_app_settings.yaml b/maestro/subflows/navigate_to_app_settings.yaml index a329ee6b5..c8be08965 100644 --- a/maestro/subflows/navigate_to_app_settings.yaml +++ b/maestro/subflows/navigate_to_app_settings.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: navigate_to_menu.yaml - tapOn: App Settings diff --git a/maestro/subflows/navigate_to_give.yaml b/maestro/subflows/navigate_to_give.yaml index df1274063..bebccb263 100644 --- a/maestro/subflows/navigate_to_give.yaml +++ b/maestro/subflows/navigate_to_give.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: ../helpers/close_open_sheet.yaml - tapOn: Give diff --git a/maestro/subflows/navigate_to_menu.yaml b/maestro/subflows/navigate_to_menu.yaml index d5015e75a..6d5fcf480 100644 --- a/maestro/subflows/navigate_to_menu.yaml +++ b/maestro/subflows/navigate_to_menu.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: ../helpers/close_open_sheet.yaml - tapOn: diff --git a/maestro/subflows/navigate_to_my_account.yaml b/maestro/subflows/navigate_to_my_account.yaml index 3574ae379..6d2a927bd 100644 --- a/maestro/subflows/navigate_to_my_account.yaml +++ b/maestro/subflows/navigate_to_my_account.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: navigate_to_menu.yaml - tapOn: My Account diff --git a/maestro/subflows/navigate_to_wallet.yaml b/maestro/subflows/navigate_to_wallet.yaml index 82d0ee77e..a89fbd939 100644 --- a/maestro/subflows/navigate_to_wallet.yaml +++ b/maestro/subflows/navigate_to_wallet.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: ../helpers/close_open_sheet.yaml - tapOn: Wallet diff --git a/maestro/subflows/navigate_to_withdraw.yaml b/maestro/subflows/navigate_to_withdraw.yaml index c042da97c..90802b890 100644 --- a/maestro/subflows/navigate_to_withdraw.yaml +++ b/maestro/subflows/navigate_to_withdraw.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: navigate_to_menu.yaml - tapOn: Withdraw Funds diff --git a/maestro/subflows/open_token_info.yaml b/maestro/subflows/open_token_info.yaml index a847a978b..6b3cb92f7 100644 --- a/maestro/subflows/open_token_info.yaml +++ b/maestro/subflows/open_token_info.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: ../helpers/close_open_sheet.yaml - tapOn: Wallet diff --git a/maestro/subflows/pull_out_bill.yaml b/maestro/subflows/pull_out_bill.yaml index 6bcaf2d4e..d2634eac3 100644 --- a/maestro/subflows/pull_out_bill.yaml +++ b/maestro/subflows/pull_out_bill.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: ../helpers/close_open_sheet.yaml - tapOn: Give diff --git a/maestro/subflows/return_to_scanner.yaml b/maestro/subflows/return_to_scanner.yaml index 722362c06..f8e8d7c93 100644 --- a/maestro/subflows/return_to_scanner.yaml +++ b/maestro/subflows/return_to_scanner.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - back - runFlow: ../helpers/close_open_sheet.yaml diff --git a/maestro/subflows/share_cashlink.yaml b/maestro/subflows/share_cashlink.yaml index 17ea30225..0efed52fa 100644 --- a/maestro/subflows/share_cashlink.yaml +++ b/maestro/subflows/share_cashlink.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - tapOn: Send as a Link # we need maestro to gain access to the content as well diff --git a/maestro/view_token_info.yaml b/maestro/view_token_info.yaml index 4893059a9..76b39cd0e 100644 --- a/maestro/view_token_info.yaml +++ b/maestro/view_token_info.yaml @@ -1,4 +1,4 @@ -appId: com.flipcash.app.android.dev +appId: com.flipcash.app.android --- - runFlow: subflows/login_with_deeplink.yaml - runFlow: subflows/open_token_info.yaml From 6455b62f7e38b373b57b149a142ada361244a8bb Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 13:27:51 -0400 Subject: [PATCH 02/19] feat(navigation): apply screen-root testTags centrally via annotatedEntry Maestro/UiAutomator flows anchor on `_screen` resource-ids, but those tags were never added to the screen composables, so the flows couldn't find them. Rather than hand-tag ~15 screen roots (which drifts), apply the tag once at the single place every destination is registered: `annotatedEntry` now wraps each entry in a tagged Box, defaulting to an id derived from the route type name (`AppRoute.Menu.MyAccount` -> `my_account_screen`) with an optional override for routes whose id differs from their name (Give -> cash_screen, Token.Info -> token_info_screen, Token.Transactions -> transaction_history_screen). Removes the now-redundant per-screen tags from TokenInfoScreen and MessengerScreen (they're covered centrally); the pre-login landing keeps a manual `login_screen` tag since it isn't a nav entry. --- .../ui/navigation/AppScreenContent.kt | 9 ++++-- .../internal/screens/LoginScreenContent.kt | 5 +++- .../internal/screens/MessengerScreen.kt | 3 +- .../app/tokens/internal/TokenInfoScreen.kt | 2 -- .../com/getcode/navigation/NavMetadata.kt | 29 ++++++++++++++++++- 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt index fd66432a9..01a33f4cf 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt @@ -92,7 +92,8 @@ fun appEntryProvider( annotatedEntry { key -> InviteContactScreen(key.phoneNumber) } // Sheets (inner content — wrapped in Main.Sheet by navigateTo()) - annotatedEntry { key -> CashScreen(key.mint, key.fromTokenInfo) } + // Route type is `Give` but the screen is the Cash/Give screen the flows call cash_screen. + annotatedEntry(testTag = "cash_screen") { key -> CashScreen(key.mint, key.fromTokenInfo) } annotatedEntry { SendFlowScreen(resultStateRegistry = resultStateRegistry) } annotatedEntry { key -> TippingFlowScreen(route = key, resultStateRegistry = resultStateRegistry) @@ -109,10 +110,12 @@ fun appEntryProvider( } // Tokens - annotatedEntry { key -> + annotatedEntry(testTag = "token_info_screen") { key -> TokenInfoScreen(key.mint, key.shortfall, key.fromDeeplink) } - annotatedEntry { key -> TransactionHistoryScreen(key.mint) } + annotatedEntry(testTag = "transaction_history_screen") { key -> + TransactionHistoryScreen(key.mint) + } annotatedEntry { key -> SwapFlowScreen(route = key, resultStateRegistry = resultStateRegistry) } diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/LoginScreenContent.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/LoginScreenContent.kt index 3368a07ce..b0d0a1f34 100644 --- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/LoginScreenContent.kt +++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/LoginScreenContent.kt @@ -53,7 +53,10 @@ internal fun LoginRouterScreenContent( Box( modifier = Modifier .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars), + .windowInsetsPadding(WindowInsets.navigationBars) + // Pre-login landing isn't a nav entry, so its screen-root tag is applied here + // (see annotatedEntry for the routed screens). + .testTag("login_screen"), ) { Column(modifier = Modifier.fillMaxSize()) { Spacer(Modifier.weight(1f)) diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt index 934bc0d39..ad61b45ff 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt @@ -114,8 +114,7 @@ private fun ChatInputScaffold( // which made the message list visibly jump on every open and every pop-back. SubcomposeLayout( modifier = Modifier - .imePadding() - .testTag("chat_screen"), + .imePadding(), ) { constraints -> val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt index f06d27c29..0a9458730 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.res.painterResource @@ -84,7 +83,6 @@ private fun TokenInfoScreen( LazyColumn( modifier = Modifier .fillMaxSize() - .testTag("token_info_screen") .padding( start = innerPadding.calculateStartPadding(), end = innerPadding.calculateEndPadding(), diff --git a/ui/navigation/src/main/kotlin/com/getcode/navigation/NavMetadata.kt b/ui/navigation/src/main/kotlin/com/getcode/navigation/NavMetadata.kt index bbbc78971..3113fddf4 100644 --- a/ui/navigation/src/main/kotlin/com/getcode/navigation/NavMetadata.kt +++ b/ui/navigation/src/main/kotlin/com/getcode/navigation/NavMetadata.kt @@ -1,7 +1,10 @@ package com.getcode.navigation import android.os.Parcelable +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import com.getcode.navigation.results.NavResultKey @@ -20,11 +23,35 @@ enum class NavMetadataKeys(val key: String, ) { /** * DSL helper: registers an entry whose metadata is derived from [T]'s marker interfaces. + * + * Every destination is wrapped in a [Box] tagged with a stable screen-root id so the whole + * screen is addressable as a single resource-id in UI tests (Maestro / UiAutomator, via + * `testTagsAsResourceId`). The tag defaults to one derived from the route type name + * ([screenRootTag], e.g. `AppRoute.Menu.MyAccount` → `my_account_screen`); pass an explicit + * [testTag] only when a route needs an id that differs from its type name. + * + * Keeping the tag here — at the one place every route is registered — means screen-root + * test anchors live in a single file and can't drift out of sync with the screens. */ inline fun EntryProviderScope.annotatedEntry( + testTag: String? = null, noinline content: @Composable (T) -> Unit ) { - entry(metadata = T::class.metadata(), content = content) + val resolvedTag = testTag ?: screenRootTag(T::class.simpleName) + val tagged: @Composable (T) -> Unit = { key -> Box(Modifier.testTag(resolvedTag)) { content(key) } } + entry(metadata = T::class.metadata(), content = tagged) +} + +/** + * Derives a screen-root test id from a route's simple type name: CamelCase becomes + * snake_case with a `_screen` suffix (e.g. `MyAccount` → `my_account_screen`, + * `Scanner` → `scanner_screen`). + */ +fun screenRootTag(simpleName: String?): String { + val base = (simpleName ?: "unknown") + .replace(Regex("([a-z0-9])([A-Z])"), "$1_$2") + .lowercase() + return "${base}_screen" } /** From e6ce57e8fbe4358f96c144557db6f32682d5605c Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 13:27:52 -0400 Subject: [PATCH 03/19] test(maestro): add verified feature flows, runner, and README - account_navigation.yaml: login -> menu -> My Account -> App Settings - wallet_token_info.yaml: login -> wallet -> token info + market-cap chart - run.sh: loads .env creds, approves App Links (fresh-install gotcha), targets a device - README: setup, the deeplink-login entry, the central tagging convention, coverage + roadmap Both flows pass green against emulator-5554. --- maestro/README.md | 103 ++++++++++++++++++++++++++++++++ maestro/account_navigation.yaml | 29 +++++++++ maestro/run.sh | 43 +++++++++++++ maestro/wallet_token_info.yaml | 20 +++++++ 4 files changed, 195 insertions(+) create mode 100644 maestro/README.md create mode 100644 maestro/account_navigation.yaml create mode 100755 maestro/run.sh create mode 100644 maestro/wallet_token_info.yaml diff --git a/maestro/README.md b/maestro/README.md new file mode 100644 index 000000000..6e130072f --- /dev/null +++ b/maestro/README.md @@ -0,0 +1,103 @@ +# Maestro E2E UI tests + +End-to-end UI flows that drive the real app on an emulator/device, in the spirit of +iOS's `FlipcashUITests`. Flows are plain YAML under `maestro/`; reusable pieces live in +`subflows/` and `helpers/`. + +## Prerequisites + +1. A booted emulator (or attached device). The suite defaults to `emulator-5554`. +2. The **debug** app installed: + ```bash + ANDROID_SERIAL=emulator-5554 ./gradlew :apps:flipcash:app:installDebug + ``` + The debug build sets `testTagsAsResourceId = true` (guarded by `BuildConfig.UI_TESTABLE` + in `App.kt`), which exposes Compose `testTag`s as resource-ids that Maestro targets with + `id:`. Release builds do **not** expose them. +3. The [Maestro CLI](https://maestro.mobile.dev) on your `PATH` (`maestro --version`). +4. Test-account credentials in `maestro/.env` (git-ignored): + ``` + SEED_PHRASE=word1 word2 ... word12 + LOGIN_DEEPLINK=https://app.flipcash.com/login?data=... + ``` + +## Running + +Use the runner — it loads `.env`, approves App Links, and targets the device: + +```bash +maestro/run.sh maestro/account_navigation.yaml +maestro/run.sh maestro/wallet_token_info.yaml maestro/view_token_info.yaml +DEVICE=emulator-5556 maestro/run.sh maestro/account_navigation.yaml # pick a device +``` + +Or invoke Maestro directly: + +```bash +maestro --device emulator-5554 test \ + -e SEED_PHRASE="..." -e LOGIN_DEEPLINK="..." maestro/account_navigation.yaml +``` + +### App Links gotcha (fresh installs) + +A freshly-installed debug build has **unverified** App Links, so `https://app.flipcash.com/...` +deeplinks open in Chrome instead of the app (you'll see "Cannot GET /login"). Approve them once +per install (the runner does this automatically): + +```bash +adb -s emulator-5554 shell pm set-app-links --package com.flipcash.app.android 2 all +``` + +### Login + +Prefer **deeplink login** (`subflows/login_with_deeplink.yaml`): it clears state and logs the +test account straight to the scanner, so every flow starts from a deterministic home screen. +Seed login (`subflows/login.yaml`) assumes a logged-out start and is only for exercising the +login screen itself. + +## Screen-root test anchors (how tagging works) + +Every routed screen is addressable by a stable `_screen` resource-id. These are applied +**centrally**, at the single place every destination is registered — `annotatedEntry` in +`AppScreenContent.kt` — not scattered across screen composables: + +- The tag defaults to one **derived from the route type name** (`screenRootTag` in + `NavMetadata.kt`): `AppRoute.Menu.MyAccount` → `my_account_screen`, + `AppRoute.Main.Scanner` → `scanner_screen`. +- Pass an explicit `testTag` only when a route needs a different id than its type name, e.g. + `annotatedEntry(testTag = "cash_screen") { ... }`. + +Because the tag lives with the route registration, adding a screen tags it automatically and +the anchors can't drift out of sync with the UI. Screens that are **not** nav entries (the +pre-login landing, inner FlowHost steps like the withdrawal wizard) still need a manual +`testTag` on their root — e.g. `login_screen` in `LoginScreenContent.kt`. + +Sub-element anchors (buttons, lists, inputs) remain plain `testTag`s in the component code — +e.g. `menu_button`, `market_cap_chart`, `chat_message_list`, `send_contact_list`, `keypad_`. + +## Coverage + +**Verified green** (deeplink-login entry): +- Login → home (`scanner_screen`) +- Menu navigation (`menu_screen`), My Account (`my_account_screen`), App Settings + (`app_settings_screen`) — `account_navigation.yaml` +- Wallet (`wallet_screen`) → Token Info (`token_info_screen`) + market-cap chart — + `wallet_token_info.yaml` +- Give / bill round-trip, token-info deeplink (existing flows) +- Screenshot suite (`screenshots/`, 19 baselines) + +**Roadmap** (each new journey = deeplink login + central anchors, plus any missing +sub-element `testTag`s): +- Repair the drifted seed-login flows (`login.yaml`, `login_logout.yaml`) — standardise on + deeplink login; the logout menu item needs a scroll-into-view. +- Direct Send (phone-number send) — `send_contact_list` / `send_contact_row` exist. +- Deposit / Add-money and Withdraw as functional flows (inner FlowHost steps need root tags). +- Token Discovery / leaderboard (`discovery_leaderboard`, `leaderboard_token_row` exist). +- Chat: send a message end-to-end (`chat_message_input`, `chat_send_icon` exist). +- Currency Creator, Swap/Buy, Onboarding / phone verification, Coinbase onramp. + +## CI + +Not yet wired. Intended: a `flipcash_maestro` Fastlane lane running +`--include-tags smoke --exclude-tags spends-funds` on a KVM emulator per PR, with the fuller +set nightly. See `docs/superpowers/plans/2026-07-01-maestro-mcp-ui-testing.md`. diff --git a/maestro/account_navigation.yaml b/maestro/account_navigation.yaml new file mode 100644 index 000000000..b5c281759 --- /dev/null +++ b/maestro/account_navigation.yaml @@ -0,0 +1,29 @@ +appId: com.flipcash.app.android +name: "Account & Settings Navigation" +tags: + - smoke + - account +--- +# Deterministic clean login → home. +- runFlow: subflows/login_with_deeplink.yaml + +# Menu → My Account +- runFlow: subflows/navigate_to_menu.yaml +- tapOn: "My Account" +- extendedWaitUntil: + visible: + id: my_account_screen + timeout: 10000 + +# Back to menu, then App Settings +- tapOn: + id: action_back +- extendedWaitUntil: + visible: + id: menu_screen + timeout: 10000 +- tapOn: "App Settings" +- extendedWaitUntil: + visible: + id: app_settings_screen + timeout: 10000 diff --git a/maestro/run.sh b/maestro/run.sh new file mode 100755 index 000000000..8d46efbf5 --- /dev/null +++ b/maestro/run.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Convenience runner for the Maestro E2E suite. +# +# Usage: +# maestro/run.sh [more flows...] +# maestro/run.sh maestro/account_navigation.yaml +# +# Handles the fiddly setup that a fresh emulator/install needs: +# - loads SEED_PHRASE / LOGIN_DEEPLINK from maestro/.env (values may contain spaces) +# - approves App Links so https deeplinks route to the app instead of the browser +# - targets a specific device when several are attached (DEVICE env, default emulator-5554) +# +# Prereqs (see maestro/README.md): emulator booted, debug app installed +# (./gradlew :apps:flipcash:app:installDebug), maestro CLI on PATH. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_ID="com.flipcash.app.android" +DEVICE="${DEVICE:-emulator-5554}" +ENV_FILE="$SCRIPT_DIR/.env" + +if [[ ! -f "$ENV_FILE" ]]; then + echo "error: $ENV_FILE not found (needs SEED_PHRASE and LOGIN_DEEPLINK)." >&2 + exit 1 +fi + +# Load creds without word-splitting the space-containing seed phrase. +SEED_PHRASE="$(grep '^SEED_PHRASE=' "$ENV_FILE" | cut -d= -f2-)" +LOGIN_DEEPLINK="$(grep '^LOGIN_DEEPLINK=' "$ENV_FILE" | cut -d= -f2-)" + +# App Links verification does not survive a fresh install; approve so +# https://app.flipcash.com/... deeplinks open the app, not Chrome. +adb -s "$DEVICE" shell pm set-app-links --package "$APP_ID" 2 all >/dev/null 2>&1 || true + +if [[ $# -eq 0 ]]; then + echo "usage: $0 [more flows...]" >&2 + exit 1 +fi + +maestro --device "$DEVICE" test \ + -e SEED_PHRASE="$SEED_PHRASE" \ + -e LOGIN_DEEPLINK="$LOGIN_DEEPLINK" \ + "$@" diff --git a/maestro/wallet_token_info.yaml b/maestro/wallet_token_info.yaml new file mode 100644 index 000000000..96b4c5d9b --- /dev/null +++ b/maestro/wallet_token_info.yaml @@ -0,0 +1,20 @@ +appId: com.flipcash.app.android +name: "Wallet → Token Info" +tags: + - smoke + - tokens +--- +# Deterministic clean login → home. +- runFlow: subflows/login_with_deeplink.yaml + +# Open the wallet and drill into a held token's info screen. +- runFlow: subflows/navigate_to_wallet.yaml +- tapOn: "Float" +- extendedWaitUntil: + visible: + id: token_info_screen + timeout: 10000 + +# The market-cap chart is always present on token info. +- assertVisible: + id: market_cap_chart From 1a66992d6a07773f96d9e46080a8b93f5010b12f Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 13:43:30 -0400 Subject: [PATCH 04/19] test(maestro): tag flow steps centrally; repair logout via My Account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flow steps register through the same annotatedEntry helper, so they get the same auto-derived screen-root tags — OnboardingStep.SeedInput already yields seed_input_screen with no per-screen edit. Add overrides only where the step name is generic/ambiguous: - OnboardingStep.Start -> login_screen (the pre-login landing) - WithdrawalStep.Amount/Destination/Confirmation -> withdraw_*_screen (the bare Destination/SelectToken names collide with the deposit flow's steps) Log Out moved to the My Account screen, so route logout.yaml through navigate_to_my_account; login_logout.yaml now starts from a cleared (logged-out) state so the real seed-login UI is exercised. Both verified green. --- .../com/flipcash/app/login/OnboardingFlowScreen.kt | 4 +++- .../login/internal/screens/LoginScreenContent.kt | 5 +---- .../flipcash/app/withdrawal/WithdrawalFlowScreen.kt | 8 +++++--- maestro/login_logout.yaml | 12 ++++++++++-- maestro/subflows/logout.yaml | 13 ++----------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt index 29eb7371d..a9f0e78bd 100644 --- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt +++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt @@ -253,7 +253,9 @@ internal fun resolvePostAccountRoute( private fun onboardingEntryProvider( route: AppRoute.OnboardingFlow, ): (NavKey) -> NavEntry = entryProvider { - annotatedEntry { step -> + // The pre-login landing; flows/screenshots anchor on `login_screen` rather than + // the step-derived `start_screen`. + annotatedEntry(testTag = "login_screen") { step -> LoginStepContent(step.seed) } annotatedEntry { diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/LoginScreenContent.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/LoginScreenContent.kt index b0d0a1f34..3368a07ce 100644 --- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/LoginScreenContent.kt +++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/LoginScreenContent.kt @@ -53,10 +53,7 @@ internal fun LoginRouterScreenContent( Box( modifier = Modifier .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars) - // Pre-login landing isn't a nav entry, so its screen-root tag is applied here - // (see annotatedEntry for the routed screens). - .testTag("login_screen"), + .windowInsetsPadding(WindowInsets.navigationBars), ) { Column(modifier = Modifier.fillMaxSize()) { Spacer(Modifier.weight(1f)) diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt index 932391875..7af45aab7 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt @@ -90,13 +90,15 @@ private fun withdrawalEntryProvider( annotatedEntry { WithdrawalSelectTokenScreen() } - annotatedEntry { step -> + // Explicit tags: step names (Amount/Destination/Confirmation) are generic and collide + // with other flows' steps, so give the E2E-targeted steps stable, unambiguous ids. + annotatedEntry(testTag = "withdraw_entry_screen") { step -> WithdrawalEntryScreen(step.mint) } - annotatedEntry { + annotatedEntry(testTag = "withdraw_destination_screen") { WithdrawalDestinationScreen() } - annotatedEntry { + annotatedEntry(testTag = "withdraw_confirmation_screen") { WithdrawalConfirmationScreen() } } diff --git a/maestro/login_logout.yaml b/maestro/login_logout.yaml index 67929ecd9..f4685f8bd 100644 --- a/maestro/login_logout.yaml +++ b/maestro/login_logout.yaml @@ -1,5 +1,13 @@ appId: com.flipcash.app.android +name: "Login (seed) + Logout" +tags: + - smoke + - onboarding --- -- runFlow: helpers/launch_app.yaml +# Start logged-out so the seed-login UI is exercised for real. +- runFlow: + file: helpers/launch_app.yaml + env: + clearAppState: "true" - runFlow: subflows/login.yaml -- runFlow: subflows/logout.yaml \ No newline at end of file +- runFlow: subflows/logout.yaml diff --git a/maestro/subflows/logout.yaml b/maestro/subflows/logout.yaml index 8de8ed5e7..13b071ada 100644 --- a/maestro/subflows/logout.yaml +++ b/maestro/subflows/logout.yaml @@ -1,16 +1,7 @@ -# Reusable Logout flow +# Reusable Logout flow. Log Out lives on the My Account screen. appId: com.flipcash.app.android --- -- tapOn: - id: scanner_view -- extendedWaitUntil: - visible: - id: scanner_screen -- tapOn: - id: menu_button -- extendedWaitUntil: - visible: - id: menu_screen +- runFlow: navigate_to_my_account.yaml - tapOn: Log Out - assertVisible: Log Out - assertVisible: Cancel From 82c68c3a79b5db5e3f7e56ef39f9ce7ccf09aab7 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 14:05:44 -0400 Subject: [PATCH 05/19] test(maestro): add send/discovery/withdraw/deposit flows; repair drifted nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New verified flows (all green against emulator-5554): - direct_send.yaml — send entry → phone gate - discovery_leaderboard.yaml — Discover → leaderboard → token info - withdraw.yaml — menu → Withdraw Money → USDC → amount entry (fund-safe) - deposit.yaml — menu → Add Money → Other Wallet → USDC deposit (fund-safe) Repair navigation subflows against relabeled UI: - navigate_to_withdraw: "Withdraw Funds" → "Withdraw Money" + USDC step + Next - navigate_to_give: nav-bar "Give" → "Cash" - navigate_to_advanced_features: "Advanced Features" → "Advanced" README: refresh coverage (7 green flows), note account-blocked journeys (Direct Send contact list / Chat need a phone-linked account) and remaining roadmap. --- maestro/README.md | 37 ++++++++++--------- maestro/deposit.yaml | 21 +++++++++++ maestro/direct_send.yaml | 29 +++++++++++++++ maestro/discovery_leaderboard.yaml | 28 ++++++++++++++ .../navigate_to_advanced_features.yaml | 2 +- maestro/subflows/navigate_to_give.yaml | 2 +- maestro/subflows/navigate_to_withdraw.yaml | 10 +++-- maestro/withdraw.yaml | 17 +++++++++ 8 files changed, 124 insertions(+), 22 deletions(-) create mode 100644 maestro/deposit.yaml create mode 100644 maestro/direct_send.yaml create mode 100644 maestro/discovery_leaderboard.yaml create mode 100644 maestro/withdraw.yaml diff --git a/maestro/README.md b/maestro/README.md index 6e130072f..b580b0099 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -77,24 +77,27 @@ e.g. `menu_button`, `market_cap_chart`, `chat_message_list`, `send_contact_list` ## Coverage -**Verified green** (deeplink-login entry): -- Login → home (`scanner_screen`) -- Menu navigation (`menu_screen`), My Account (`my_account_screen`), App Settings - (`app_settings_screen`) — `account_navigation.yaml` -- Wallet (`wallet_screen`) → Token Info (`token_info_screen`) + market-cap chart — - `wallet_token_info.yaml` -- Give / bill round-trip, token-info deeplink (existing flows) -- Screenshot suite (`screenshots/`, 19 baselines) - -**Roadmap** (each new journey = deeplink login + central anchors, plus any missing -sub-element `testTag`s): -- Repair the drifted seed-login flows (`login.yaml`, `login_logout.yaml`) — standardise on - deeplink login; the logout menu item needs a scroll-into-view. -- Direct Send (phone-number send) — `send_contact_list` / `send_contact_row` exist. -- Deposit / Add-money and Withdraw as functional flows (inner FlowHost steps need root tags). -- Token Discovery / leaderboard (`discovery_leaderboard`, `leaderboard_token_row` exist). -- Chat: send a message end-to-end (`chat_message_input`, `chat_send_icon` exist). +**Verified green** (run any of these with `maestro/run.sh`): +- `login_logout.yaml` — real seed-login UI + logout (Log Out lives on My Account) +- `account_navigation.yaml` — menu → My Account → App Settings +- `wallet_token_info.yaml` — wallet → token info + market-cap chart +- `discovery_leaderboard.yaml` — Discover → leaderboard → token info +- `direct_send.yaml` — send entry → phone gate +- `withdraw.yaml` — menu → Withdraw Money → USDC → amount entry (fund-safe) +- `deposit.yaml` — menu → Add Money → Other Wallet → USDC deposit (fund-safe) +- Give/bill round-trip, token-info deeplink, screenshot suite (existing) + +**Account-blocked** (this test account has no phone linked, so it can't reach these; wiring +is ready — the screens are tagged): +- Direct Send contact list (`send_contact_list`/`send_contact_row`) — gated by phone link. +- Chat send-message (`chat_screen`, `chat_message_input`, `chat_send_icon`) — needs a + conversation, which needs contacts. + +**Roadmap** (each = deeplink login + central anchors, plus any missing sub-element `testTag`s): +- Withdraw/Deposit past the amount step (needs a funded reserves balance; destination / + confirmation steps are already tagged). - Currency Creator, Swap/Buy, Onboarding / phone verification, Coinbase onramp. +- Wire a `flipcash_maestro` Fastlane lane (see below). ## CI diff --git a/maestro/deposit.yaml b/maestro/deposit.yaml new file mode 100644 index 000000000..b0e3a397a --- /dev/null +++ b/maestro/deposit.yaml @@ -0,0 +1,21 @@ +appId: com.flipcash.app.android +name: "Deposit (Add Money) → USDC" +tags: + - smoke + - deposit +--- +# Fund-safe: opens the Add Money method sheet and enters the USDC deposit flow. +- runFlow: subflows/login_with_deeplink.yaml +- runFlow: subflows/navigate_to_menu.yaml + +- tapOn: "Add Money" +- extendedWaitUntil: + visible: + text: "Select Method" + timeout: 8000 +- tapOn: "Other Wallet" +- extendedWaitUntil: + visible: + id: deposit_screen + timeout: 8000 +- assertVisible: "Deposit USDC" diff --git a/maestro/direct_send.yaml b/maestro/direct_send.yaml new file mode 100644 index 000000000..de7a1abec --- /dev/null +++ b/maestro/direct_send.yaml @@ -0,0 +1,29 @@ +appId: com.flipcash.app.android +name: "Direct Send — entry & phone gate" +tags: + - smoke + - payments +--- +# Deterministic clean login → home. +- runFlow: subflows/login_with_deeplink.yaml + +# Open the send flow from the scanner nav bar. +- tapOn: "Send" +- extendedWaitUntil: + visible: + id: send_screen + timeout: 8000 + +# This test account has no phone linked for send, so the flow opens on the phone +# gate (SendStep.PhoneGate). phone_gate_screen is auto-tagged from the step name. +- assertVisible: + id: phone_gate_screen +- assertVisible: "Send Money To Your Friends" + +# Close the sheet and return home. +- tapOn: + id: action_close +- extendedWaitUntil: + visible: + id: scanner_screen + timeout: 8000 diff --git a/maestro/discovery_leaderboard.yaml b/maestro/discovery_leaderboard.yaml new file mode 100644 index 000000000..ebdc091c3 --- /dev/null +++ b/maestro/discovery_leaderboard.yaml @@ -0,0 +1,28 @@ +appId: com.flipcash.app.android +name: "Token Discovery — leaderboard → token info" +tags: + - smoke + - tokens +--- +# Deterministic clean login → home. +- runFlow: subflows/login_with_deeplink.yaml + +# Open Discover and assert the leaderboard rendered. +- tapOn: "Discover" +- extendedWaitUntil: + visible: + id: discovery_screen + timeout: 8000 +- assertVisible: + id: discovery_leaderboard +- assertVisible: + id: leaderboard_token_row + +# Drill into the top token's info screen. +- tapOn: + id: leaderboard_token_row + index: 0 +- extendedWaitUntil: + visible: + id: token_info_screen + timeout: 8000 diff --git a/maestro/subflows/navigate_to_advanced_features.yaml b/maestro/subflows/navigate_to_advanced_features.yaml index c7ca8ea35..d9c295af9 100644 --- a/maestro/subflows/navigate_to_advanced_features.yaml +++ b/maestro/subflows/navigate_to_advanced_features.yaml @@ -1,7 +1,7 @@ appId: com.flipcash.app.android --- - runFlow: navigate_to_menu.yaml -- tapOn: Advanced Features +- tapOn: Advanced - extendedWaitUntil: visible: id: advanced_features_screen diff --git a/maestro/subflows/navigate_to_give.yaml b/maestro/subflows/navigate_to_give.yaml index bebccb263..148d3d452 100644 --- a/maestro/subflows/navigate_to_give.yaml +++ b/maestro/subflows/navigate_to_give.yaml @@ -1,7 +1,7 @@ appId: com.flipcash.app.android --- - runFlow: ../helpers/close_open_sheet.yaml -- tapOn: Give +- tapOn: Cash - extendedWaitUntil: visible: id: cash_screen diff --git a/maestro/subflows/navigate_to_withdraw.yaml b/maestro/subflows/navigate_to_withdraw.yaml index 90802b890..08924e9b3 100644 --- a/maestro/subflows/navigate_to_withdraw.yaml +++ b/maestro/subflows/navigate_to_withdraw.yaml @@ -1,11 +1,15 @@ +# Menu → Withdraw Money → (USDC path) → amount entry. appId: com.flipcash.app.android --- - runFlow: navigate_to_menu.yaml -- tapOn: Withdraw Funds +- tapOn: "Withdraw Money" - extendedWaitUntil: visible: - text: Select Currency -- tapOn: Float + id: withdrawal_screen + timeout: 8000 +- tapOn: "Withdraw as USDC" +- tapOn: "Next" - extendedWaitUntil: visible: id: withdraw_entry_screen + timeout: 8000 diff --git a/maestro/withdraw.yaml b/maestro/withdraw.yaml new file mode 100644 index 000000000..cef70dc16 --- /dev/null +++ b/maestro/withdraw.yaml @@ -0,0 +1,17 @@ +appId: com.flipcash.app.android +name: "Withdraw — reach amount entry" +tags: + - smoke + - withdraw +--- +# Fund-safe: drives the withdrawal wizard to the amount-entry screen. Reaching the +# destination/confirmation steps needs a funded reserves balance, so those steps +# (withdraw_destination_screen / withdraw_confirmation_screen, tagged in the app) are +# left for a funded account. +- runFlow: subflows/login_with_deeplink.yaml +- runFlow: subflows/navigate_to_withdraw.yaml + +- assertVisible: + id: withdraw_entry_screen +- assertVisible: + id: keypad_5 From a61560b78fab84f6df7f528f3fe269540f3f8669 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 14:34:50 -0400 Subject: [PATCH 06/19] feat(featureflags): enable beta flags via launch arg for UI tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a debug-only (UI_TESTABLE) MainActivity hook that reads a `betaFlags` launch argument (comma-separated FeatureFlag.key list) and force-enables those flags, so UI tests can exercise beta-gated features without toggling them in the Labs UI — mirroring iOS's --beta-flags. Maestro support: - subflows/login_with_flags.yaml — seed login with flags (existing account) - subflows/create_account.yaml — new account via onboarding (test phone +1 500-555-0000, all-zero OTP) with optional push-permission handling, for one-run-per-account setup - tipping_setup.yaml — create account with tipping_enabled, set up the tip card (name step), assert the tip card renders. Verified green. - run.sh forwards BETA_FLAGS; README documents the mechanism. --- .../kotlin/com/flipcash/app/MainActivity.kt | 28 ++++++++++++++++ maestro/README.md | 26 +++++++++++++++ maestro/run.sh | 1 + maestro/subflows/create_account.yaml | 33 +++++++++++++++++++ maestro/subflows/login_with_flags.yaml | 11 +++++++ maestro/tipping_setup.yaml | 28 ++++++++++++++++ 6 files changed, 127 insertions(+) create mode 100644 maestro/subflows/create_account.yaml create mode 100644 maestro/subflows/login_with_flags.yaml create mode 100644 maestro/tipping_setup.yaml diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/MainActivity.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/MainActivity.kt index 03501dfef..2ed746a1f 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/MainActivity.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/MainActivity.kt @@ -29,6 +29,7 @@ import com.flipcash.app.core.verification.email.EmailCodeChannel import com.flipcash.app.core.verification.email.LocalEmailCodeChannel import com.flipcash.app.onramp.LocalCoinbaseOnRampController import com.flipcash.app.onramp.CoinbaseOnRampController +import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.FeatureFlagController import com.flipcash.app.featureflags.LocalFeatureFlags import com.flipcash.app.internal.ui.App @@ -151,6 +152,8 @@ class MainActivity : FragmentActivity() { // the UI thread building the country list. lifecycleScope.launch(Dispatchers.Default) { phoneUtils.ensureLoaded() } + applyBetaFlagLaunchOverrides() + setContent { CompositionLocalProvider( LocalResources provides resources, @@ -187,8 +190,33 @@ class MainActivity : FragmentActivity() { } } + /** + * Test-only: enable beta flags passed as a launch argument, so flag-gated features + * (tipping, blocklist, …) can be exercised in UI tests without toggling them in the + * Labs UI. Mirrors iOS's `--beta-flags`. Debug/UI-test builds only. + * + * launchApp: + * arguments: + * betaFlags: "tipping_enabled,blocklist_enabled" + * + * The value is a comma-separated list of [FeatureFlag.key]s. + */ + private fun applyBetaFlagLaunchOverrides() { + if (!BuildConfig.UI_TESTABLE) return + intent.getStringExtra(BETA_FLAGS) + .orEmpty() + .split(",") + .map { it.trim() } + .filter { it.isNotEmpty() } + .forEach { key -> + FeatureFlag.entries.firstOrNull { it.key == key } + ?.let { featureFlagController.set(it, true) } + } + } + companion object { private const val UI_TEST = "isUiTest" + private const val BETA_FLAGS = "betaFlags" } } diff --git a/maestro/README.md b/maestro/README.md index b580b0099..89f88204c 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -75,6 +75,31 @@ pre-login landing, inner FlowHost steps like the withdrawal wizard) still need a Sub-element anchors (buttons, lists, inputs) remain plain `testTag`s in the component code — e.g. `menu_button`, `market_cap_chart`, `chat_message_list`, `send_contact_list`, `keypad_`. +## Enabling beta flags from a test + +Beta-gated features (Tipping, Blocklist, …) can be turned on **at launch** without toggling +them in the Labs UI — mirroring iOS's `--beta-flags`. Pass a `betaFlags` launch argument (a +comma-separated list of `FeatureFlag.key`s); `MainActivity` reads it on debug/UI-test builds +and force-enables those flags: + +```yaml +- launchApp: + arguments: + isUiTest: true + betaFlags: "tipping_enabled,blocklist_enabled" +``` + +The overrides must be applied in the **same process** that renders the feature — deeplink +login relaunches via `openLink` and would drop the argument. So use one of: +- `subflows/login_with_flags.yaml` — seed login into the **existing** account with flags set. +- `subflows/create_account.yaml` — a brand-new account through onboarding (test phone + `+1 (500) 555-0000`, all-zero OTP), for one-run-per-account setup like the tip card. Both + take a `BETA_FLAGS` env var; the runner forwards `BETA_FLAGS` from your shell. + +```bash +BETA_FLAGS=tipping_enabled maestro/run.sh maestro/tipping_setup.yaml +``` + ## Coverage **Verified green** (run any of these with `maestro/run.sh`): @@ -85,6 +110,7 @@ e.g. `menu_button`, `market_cap_chart`, `chat_message_list`, `send_contact_list` - `direct_send.yaml` — send entry → phone gate - `withdraw.yaml` — menu → Withdraw Money → USDC → amount entry (fund-safe) - `deposit.yaml` — menu → Add Money → Other Wallet → USDC deposit (fund-safe) +- `tipping_setup.yaml` — create account (beta flag) → set up tip card → tip card renders - Give/bill round-trip, token-info deeplink, screenshot suite (existing) **Account-blocked** (this test account has no phone linked, so it can't reach these; wiring diff --git a/maestro/run.sh b/maestro/run.sh index 8d46efbf5..b0c5a612b 100755 --- a/maestro/run.sh +++ b/maestro/run.sh @@ -40,4 +40,5 @@ fi maestro --device "$DEVICE" test \ -e SEED_PHRASE="$SEED_PHRASE" \ -e LOGIN_DEEPLINK="$LOGIN_DEEPLINK" \ + -e BETA_FLAGS="${BETA_FLAGS:-}" \ "$@" diff --git a/maestro/subflows/create_account.yaml b/maestro/subflows/create_account.yaml new file mode 100644 index 000000000..0fb8912d9 --- /dev/null +++ b/maestro/subflows/create_account.yaml @@ -0,0 +1,33 @@ +# Create a fresh account through onboarding, for flows that must run on a brand-new +# account (e.g. one-run-per-account setup). Uses the test phone +1 (500) 555-0000 and +# the all-zero OTP. Enables beta flags at launch via BETA_FLAGS (comma-separated +# FeatureFlag.key list; debug builds only). +appId: com.flipcash.app.android +--- +- clearState +- launchApp: + arguments: + isUiTest: true + betaFlags: ${BETA_FLAGS} + +- extendedWaitUntil: { visible: { id: login_screen }, timeout: 15000 } +- tapOn: { id: create_account_button } + +# Phone verification (test number + all-zero OTP). +- extendedWaitUntil: { visible: { id: phone_entry_screen }, timeout: 15000 } +- inputText: "5005550000" +- tapOn: "Next" +- extendedWaitUntil: { visible: { id: phone_code_screen }, timeout: 15000 } +- inputText: "000000" + +# Access key — take the "wrote it down" path (confirm the dialog). +- extendedWaitUntil: { visible: { id: access_key_screen }, timeout: 15000 } +- tapOn: "Wrote the 12 Words Down Instead?" +- tapOn: "Wrote the 12 Words Down Instead?" + +# A push-notification permission dialog appears only if it isn't already granted. +- tapOn: + text: "Allow" + optional: true + +- extendedWaitUntil: { visible: { id: scanner_screen }, timeout: 20000 } diff --git a/maestro/subflows/login_with_flags.yaml b/maestro/subflows/login_with_flags.yaml new file mode 100644 index 000000000..c9f908391 --- /dev/null +++ b/maestro/subflows/login_with_flags.yaml @@ -0,0 +1,11 @@ +# Seed login that also enables beta flags via a launch argument (debug builds only). +# Seed login stays in one process (no relaunch), so flags applied in onCreate persist. +# Requires env: SEED_PHRASE and BETA_FLAGS (comma-separated FeatureFlag.key list). +appId: com.flipcash.app.android +--- +- clearState +- launchApp: + arguments: + isUiTest: true + betaFlags: ${BETA_FLAGS} +- runFlow: login.yaml diff --git a/maestro/tipping_setup.yaml b/maestro/tipping_setup.yaml new file mode 100644 index 000000000..80cb63350 --- /dev/null +++ b/maestro/tipping_setup.yaml @@ -0,0 +1,28 @@ +appId: com.flipcash.app.android +name: "Tipping — create account & set up tip card" +tags: + - tipping +--- +# Tip setup is one-run-per-account, so start from a brand-new account with the tipping +# beta flag enabled at launch (no Labs-UI toggling). +- runFlow: + file: subflows/create_account.yaml + env: + BETA_FLAGS: "tipping_enabled" + +# Tipping is enabled -> the Tips tab is present. +- assertVisible: "Tips" +- tapOn: "Tips" +- extendedWaitUntil: { visible: { id: tips_screen }, timeout: 8000 } + +# Set up the tip card: name is the next step. +- tapOn: "Start Receiving Tips" +- extendedWaitUntil: { visible: { id: name_screen }, timeout: 8000 } +- tapOn: { text: "Your Name" } +- inputText: "Test Tipper" +- tapOn: "Next" + +# The tip card renders with the chosen name. +- extendedWaitUntil: { visible: { id: tip_card_screen }, timeout: 8000 } +- assertVisible: "My Tip Card" +- assertVisible: "Tip Test Tipper" From 4b89aa960a07c798d8d8df11ab698b998fa144a4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 14:50:57 -0400 Subject: [PATCH 07/19] test(maestro): add tip-chat flow (open conversation + send message) Seed-logs into the tips account with tipping_enabled, opens the tip conversation from the Tips tab, asserts the tip event + composer, and sends a message. Verified green (requires SEED_PHRASE and LOGIN_DEEPLINK to point at the same tip-enabled test account). --- maestro/README.md | 1 + maestro/tip_chat.yaml | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 maestro/tip_chat.yaml diff --git a/maestro/README.md b/maestro/README.md index 89f88204c..b78d845f9 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -111,6 +111,7 @@ BETA_FLAGS=tipping_enabled maestro/run.sh maestro/tipping_setup.yaml - `withdraw.yaml` — menu → Withdraw Money → USDC → amount entry (fund-safe) - `deposit.yaml` — menu → Add Money → Other Wallet → USDC deposit (fund-safe) - `tipping_setup.yaml` — create account (beta flag) → set up tip card → tip card renders +- `tip_chat.yaml` — open the tip conversation from the Tips tab and send a message - Give/bill round-trip, token-info deeplink, screenshot suite (existing) **Account-blocked** (this test account has no phone linked, so it can't reach these; wiring diff --git a/maestro/tip_chat.yaml b/maestro/tip_chat.yaml new file mode 100644 index 000000000..75a6ba06f --- /dev/null +++ b/maestro/tip_chat.yaml @@ -0,0 +1,28 @@ +appId: com.flipcash.app.android +name: "Tip chat — open conversation & send a message" +tags: + - tipping +--- +# Seed-login into the test account (which has tips set up + a tip chat) with the +# tipping beta flag enabled at launch. +- runFlow: + file: subflows/login_with_flags.yaml + env: + BETA_FLAGS: "tipping_enabled" + +# Tips tab lists tip conversations; open the first one. +- tapOn: "Tips" +- extendedWaitUntil: { visible: { id: tips_screen }, timeout: 8000 } +- assertVisible: { id: send_contact_row } +- tapOn: { id: send_contact_row, index: 0 } + +# In the tip conversation: the tip event and the message composer are present. +- extendedWaitUntil: { visible: { id: chat_screen }, timeout: 8000 } +- assertVisible: { id: chat_message_input } +- assertVisible: "You tipped" + +# Send a message and confirm it lands in the transcript. +- tapOn: { id: chat_message_input } +- inputText: "gg" +- tapOn: { id: chat_send_icon } +- extendedWaitUntil: { visible: { text: "gg" }, timeout: 8000 } From 634e72963fea6edfb9923f665ff794e04a60c94a Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 15:03:45 -0400 Subject: [PATCH 08/19] test(maestro): add blocking flow (block + unblock a chat participant) Enables tipping+blocklist at launch, opens the tip conversation, blocks the participant from their profile, verifies they appear in My Account -> Blocked, then unblocks to restore a clean account. Verified green. --- maestro/README.md | 2 ++ maestro/blocking.yaml | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 maestro/blocking.yaml diff --git a/maestro/README.md b/maestro/README.md index b78d845f9..d2de62b51 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -112,6 +112,8 @@ BETA_FLAGS=tipping_enabled maestro/run.sh maestro/tipping_setup.yaml - `deposit.yaml` — menu → Add Money → Other Wallet → USDC deposit (fund-safe) - `tipping_setup.yaml` — create account (beta flag) → set up tip card → tip card renders - `tip_chat.yaml` — open the tip conversation from the Tips tab and send a message +- `blocking.yaml` — block a chat participant from their profile, verify in My Account → + Blocked, then unblock (leaves the account clean) - Give/bill round-trip, token-info deeplink, screenshot suite (existing) **Account-blocked** (this test account has no phone linked, so it can't reach these; wiring diff --git a/maestro/blocking.yaml b/maestro/blocking.yaml new file mode 100644 index 000000000..a8979c44a --- /dev/null +++ b/maestro/blocking.yaml @@ -0,0 +1,42 @@ +appId: com.flipcash.app.android +name: "Blocking — block & unblock a chat participant" +tags: + - blocklist +--- +# Enable tipping (to reach the tip chat) + blocklist at launch. +- runFlow: + file: subflows/login_with_flags.yaml + env: + BETA_FLAGS: "tipping_enabled,blocklist_enabled" + +# Open the tip conversation. +- tapOn: "Tips" +- extendedWaitUntil: { visible: { id: tips_screen }, timeout: 8000 } +- tapOn: { id: send_contact_row, index: 0 } +- extendedWaitUntil: { visible: { id: chat_screen }, timeout: 8000 } + +# Open the participant's profile from the chat header and block them. +# (Participant name is specific to the test account's tip chat.) +- tapOn: { text: "Brandon McAnsh", index: 0 } +- extendedWaitUntil: { visible: { id: profile_screen }, timeout: 8000 } +- tapOn: "Block" +- assertVisible: "Block Brandon McAnsh?" +- tapOn: { text: "^Block$" } +- extendedWaitUntil: { visible: { id: tips_screen }, timeout: 8000 } + +# Verify they appear in My Account -> Blocked. +- tapOn: { id: action_close, optional: true } +- extendedWaitUntil: { visible: { id: scanner_screen }, timeout: 8000 } +- tapOn: { id: menu_button } +- extendedWaitUntil: { visible: { id: menu_screen }, timeout: 8000 } +- tapOn: "My Account" +- extendedWaitUntil: { visible: { id: my_account_screen }, timeout: 8000 } +- tapOn: "Blocked" +- extendedWaitUntil: { visible: { id: blocklist_screen }, timeout: 8000 } +- assertVisible: "Brandon McAnsh" + +# Unblock to restore the account to a clean state. +- tapOn: "Brandon McAnsh" +- assertVisible: "Unblock Brandon McAnsh?" +- tapOn: { text: "^Unblock$" } +- assertVisible: "No One Blocked" From d15535eb56705bb2a59f752b8a394fc6d21a411b Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 15:22:07 -0400 Subject: [PATCH 09/19] test(maestro): add tip-deeplink flow (opens the tip flow) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opens a tip-card deeplink (TIPCARD_DEEPLINK from .env) and asserts the tip flow presents. A fresh login clears the local balance cache, so the flow forces a balance sync (opens the Wallet) before opening the deeplink — otherwise the transiently-empty giveable balance trips the add-money/discover gate. run.sh forwards TIPCARD_DEEPLINK. --- maestro/README.md | 2 ++ maestro/run.sh | 2 ++ maestro/tip_deeplink.yaml | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 maestro/tip_deeplink.yaml diff --git a/maestro/README.md b/maestro/README.md index d2de62b51..772df10ca 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -114,6 +114,8 @@ BETA_FLAGS=tipping_enabled maestro/run.sh maestro/tipping_setup.yaml - `tip_chat.yaml` — open the tip conversation from the Tips tab and send a message - `blocking.yaml` — block a chat participant from their profile, verify in My Account → Blocked, then unblock (leaves the account clean) +- `tip_deeplink.yaml` — open a tip-card deeplink (`TIPCARD_DEEPLINK`) → presents the tip flow + (waits for balances to sync first, else the empty-cache state trips the add-money gate) - Give/bill round-trip, token-info deeplink, screenshot suite (existing) **Account-blocked** (this test account has no phone linked, so it can't reach these; wiring diff --git a/maestro/run.sh b/maestro/run.sh index b0c5a612b..0991bde69 100755 --- a/maestro/run.sh +++ b/maestro/run.sh @@ -27,6 +27,7 @@ fi # Load creds without word-splitting the space-containing seed phrase. SEED_PHRASE="$(grep '^SEED_PHRASE=' "$ENV_FILE" | cut -d= -f2-)" LOGIN_DEEPLINK="$(grep '^LOGIN_DEEPLINK=' "$ENV_FILE" | cut -d= -f2-)" +TIPCARD_DEEPLINK="$(grep '^TIPCARD_DEEPLINK=' "$ENV_FILE" | cut -d= -f2-)" # App Links verification does not survive a fresh install; approve so # https://app.flipcash.com/... deeplinks open the app, not Chrome. @@ -40,5 +41,6 @@ fi maestro --device "$DEVICE" test \ -e SEED_PHRASE="$SEED_PHRASE" \ -e LOGIN_DEEPLINK="$LOGIN_DEEPLINK" \ + -e TIPCARD_DEEPLINK="$TIPCARD_DEEPLINK" \ -e BETA_FLAGS="${BETA_FLAGS:-}" \ "$@" diff --git a/maestro/tip_deeplink.yaml b/maestro/tip_deeplink.yaml new file mode 100644 index 000000000..799c4ca5d --- /dev/null +++ b/maestro/tip_deeplink.yaml @@ -0,0 +1,23 @@ +appId: com.flipcash.app.android +name: "Tip deeplink — opens the tip flow" +tags: + - tipping +--- +# Seed-login with tipping enabled. A fresh login clears local state, so give balances +# a moment to sync (opening the Wallet forces the token balances to load) before opening +# the deeplink — otherwise hasGiveableBalance is transiently false and the app shows the +# add-money/discover gate instead of the tip flow. +- runFlow: + file: subflows/login_with_flags.yaml + env: + BETA_FLAGS: "tipping_enabled" +- runFlow: subflows/navigate_to_wallet.yaml +- assertVisible: { id: wallet_screen } + +# Open the tip card deeplink -> presents the tip flow for that recipient. +- openLink: ${TIPCARD_DEEPLINK} +- extendedWaitUntil: + visible: + text: "Swipe to Tip" + timeout: 12000 +- assertVisible: "Tip Brandon McAnsh" From 674f40c054b6b192ec97096fd8869194020cc491 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 16:02:07 -0400 Subject: [PATCH 10/19] feat(funding): tag purchase-method buttons for UI tests Add an optional testTag to BottomBarAction (applied at render in BottomBarContainer) and set it on the Add Money purchase methods: purchase_method_coinbase / purchase_method_phantom / purchase_method_other_wallet. The Coinbase (Google Pay) option is icon-only with no text, so it wasn't addressable by Maestro before. --- .../main/kotlin/com/flipcash/app/funding/internal/Buttons.kt | 5 +++++ .../src/main/kotlin/com/getcode/manager/BottomBarManager.kt | 4 ++++ .../com/getcode/ui/components/bars/BottomBarContainer.kt | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt index a132bc0cb..11ee5a121 100644 --- a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt +++ b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt @@ -51,6 +51,7 @@ internal fun purchaseOptions( width = 150.sp, height = 20.sp, tintIcon = false, + testTag = "purchase_method_coinbase", onClick = { onClick(PurchaseMethod.CoinbaseOnRamp) } ) ) @@ -76,6 +77,7 @@ internal fun purchaseOptions( suffix = resources.getString(R.string.label_phantom), iconPadding = { PaddingValues() }, iconRes = R.drawable.ic_phantom_wallet, + testTag = "purchase_method_phantom", onClick = { onClick(PurchaseMethod.PhantomWallet) } ) ) @@ -84,6 +86,7 @@ internal fun purchaseOptions( add( BottomBarAction( text = resources.getString(R.string.title_onrampProviderOtherWallet), + testTag = "purchase_method_other_wallet", onClick = { onClick(PurchaseMethod.OtherWallet) } ) ) @@ -111,9 +114,11 @@ private fun buildButtonAction( ) }, tintIcon: Boolean = true, + testTag: String? = null, onClick: () -> Unit ): BottomBarAction { return BottomBarAction( + testTag = testTag, text = buildAnnotatedString { if (prefix != null) { append(prefix) diff --git a/libs/messaging/src/main/kotlin/com/getcode/manager/BottomBarManager.kt b/libs/messaging/src/main/kotlin/com/getcode/manager/BottomBarManager.kt index 68055b6e0..e31419906 100644 --- a/libs/messaging/src/main/kotlin/com/getcode/manager/BottomBarManager.kt +++ b/libs/messaging/src/main/kotlin/com/getcode/manager/BottomBarManager.kt @@ -12,6 +12,8 @@ data class BottomBarAction( val style: BottomBarManager.BottomBarButtonStyle = BottomBarManager.BottomBarButtonStyle.Filled, val isUser: Boolean = true, val enabled: Boolean = true, + // Optional UI-test anchor; surfaced as a resource-id when testTagsAsResourceId is on. + val testTag: String? = null, val onClick: () -> Unit = { } ) { constructor( @@ -19,6 +21,7 @@ data class BottomBarAction( style: BottomBarManager.BottomBarButtonStyle = BottomBarManager.BottomBarButtonStyle.Filled, isUser: Boolean = true, enabled: Boolean = true, + testTag: String? = null, onClick: () -> Unit = { } ) : this( text = AnnotatedString(text), @@ -26,6 +29,7 @@ data class BottomBarAction( style = style, isUser = isUser, enabled = enabled, + testTag = testTag, onClick = onClick ) diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt index fe3f308f2..b9253d4a1 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt @@ -39,6 +39,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.Color @@ -306,6 +307,9 @@ fun BottomBarView( CodeButton( modifier = Modifier .fillMaxWidth() + .addIf(action.testTag != null) { + Modifier.testTag(action.testTag!!) + } .addIf(index == actions.lastIndex) { Modifier.padding( bottom = when (action.style) { From bccacb226064042b4f671cd5dfe24883e8ac537d Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 16:02:08 -0400 Subject: [PATCH 11/19] test(maestro): add buy, sell, currency-creator, coinbase-onramp flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buy.yaml: token info → Buy → payment currency → confirm purchase (fund-safe) - sell.yaml: token info → Sell → amount entry (fund-safe) - currency_creator.yaml: Discover → Create Your Own Currency → intro + $20 gate - coinbase_onramp.yaml: Add Money → Coinbase/Google Pay → onramp (phone verify), with coinbase_onramp_sandbox_enabled set for a future sandbox purchase All verified green against emulator-5554. --- maestro/README.md | 23 +++++++++++++---------- maestro/buy.yaml | 26 ++++++++++++++++++++++++++ maestro/coinbase_onramp.yaml | 23 +++++++++++++++++++++++ maestro/currency_creator.yaml | 19 +++++++++++++++++++ maestro/sell.yaml | 17 +++++++++++++++++ 5 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 maestro/buy.yaml create mode 100644 maestro/coinbase_onramp.yaml create mode 100644 maestro/currency_creator.yaml create mode 100644 maestro/sell.yaml diff --git a/maestro/README.md b/maestro/README.md index 772df10ca..5eebb3fae 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -116,18 +116,21 @@ BETA_FLAGS=tipping_enabled maestro/run.sh maestro/tipping_setup.yaml Blocked, then unblock (leaves the account clean) - `tip_deeplink.yaml` — open a tip-card deeplink (`TIPCARD_DEEPLINK`) → presents the tip flow (waits for balances to sync first, else the empty-cache state trips the add-money gate) +- `buy.yaml` — token info → Buy → payment currency → confirm-purchase screen (fund-safe) +- `sell.yaml` — token info → Sell → amount entry (fund-safe) +- `currency_creator.yaml` — Discover → Create Your Own Currency → intro + $20 balance gate +- `coinbase_onramp.yaml` — Add Money → Coinbase/Google Pay method → onramp (phone verify); + `coinbase_onramp_sandbox_enabled` set so a follow-up can drive a sandbox purchase - Give/bill round-trip, token-info deeplink, screenshot suite (existing) -**Account-blocked** (this test account has no phone linked, so it can't reach these; wiring -is ready — the screens are tagged): -- Direct Send contact list (`send_contact_list`/`send_contact_row`) — gated by phone link. -- Chat send-message (`chat_screen`, `chat_message_input`, `chat_send_icon`) — needs a - conversation, which needs contacts. - -**Roadmap** (each = deeplink login + central anchors, plus any missing sub-element `testTag`s): -- Withdraw/Deposit past the amount step (needs a funded reserves balance; destination / - confirmation steps are already tagged). -- Currency Creator, Swap/Buy, Onboarding / phone verification, Coinbase onramp. +**Roadmap / not-yet-covered:** +- **Full Coinbase sandbox purchase** — the flow reaches the onramp; completing a purchase needs + phone verification first (which links a phone to the shared account — a side effect that would + change the send flows) and driving the Google Pay sandbox sheet. Sandbox flag + method tag are + in place. +- **Send-to-contact** (send cash + message to a Flipcash contact) — needs a phone-linked account + with contacts; `send_contact_list`/`send_contact_row` are tagged and ready. +- Buy/Sell/Withdraw past confirmation on a funded account (screens tagged). - Wire a `flipcash_maestro` Fastlane lane (see below). ## CI diff --git a/maestro/buy.yaml b/maestro/buy.yaml new file mode 100644 index 000000000..eccd7db46 --- /dev/null +++ b/maestro/buy.yaml @@ -0,0 +1,26 @@ +appId: com.flipcash.app.android +name: "Buy — token info → confirm purchase (fund-safe)" +tags: + - smoke + - swap +--- +# Fund-safe: drives the buy flow to the confirmation screen and stops (never confirms). +- runFlow: subflows/login_with_deeplink.yaml +- runFlow: subflows/navigate_to_wallet.yaml + +- tapOn: "Float" +- extendedWaitUntil: { visible: { id: token_info_screen }, timeout: 8000 } +- tapOn: "Buy" +- extendedWaitUntil: { visible: { id: swap_screen }, timeout: 8000 } +- assertVisible: "Amount to Buy" + +# Minimal amount, then pick a payment currency. +- tapOn: { id: keypad_1 } +- tapOn: "Next" +- extendedWaitUntil: { visible: { id: token_selection_screen }, timeout: 8000 } +- assertVisible: "Select Payment Currency" +- tapOn: "USDF" + +# Confirmation — assert and stop (do not confirm the purchase). +- extendedWaitUntil: { visible: { id: buy_receipt_screen }, timeout: 8000 } +- assertVisible: "Confirm Purchase" diff --git a/maestro/coinbase_onramp.yaml b/maestro/coinbase_onramp.yaml new file mode 100644 index 000000000..56386174f --- /dev/null +++ b/maestro/coinbase_onramp.yaml @@ -0,0 +1,23 @@ +appId: com.flipcash.app.android +name: "Coinbase onramp — method present, enters onramp (sandbox)" +tags: + - deposit + - coinbase +--- +# Sandbox flag on so a follow-up can drive a test purchase without real funds. +- runFlow: + file: subflows/login_with_flags.yaml + env: + BETA_FLAGS: "coinbase_onramp_sandbox_enabled" + +- runFlow: subflows/navigate_to_menu.yaml +- tapOn: "Add Money" +- extendedWaitUntil: { visible: { text: "Select Method" }, timeout: 8000 } + +# The Coinbase / Google Pay method is icon-only, so it's anchored by testTag. +- assertVisible: { id: purchase_method_coinbase } +- tapOn: { id: purchase_method_coinbase } + +# Coinbase requires a verified phone, so the onramp begins with phone verification. +- extendedWaitUntil: { visible: { id: verification_screen }, timeout: 8000 } +- assertVisible: "Connect Phone Number" diff --git a/maestro/currency_creator.yaml b/maestro/currency_creator.yaml new file mode 100644 index 000000000..218475094 --- /dev/null +++ b/maestro/currency_creator.yaml @@ -0,0 +1,19 @@ +appId: com.flipcash.app.android +name: "Currency Creator — intro & balance gate" +tags: + - smoke + - currency-creator +--- +# Fund-safe: opens the creator and confirms the $20-fee balance gate. Never pays. +- runFlow: subflows/login_with_deeplink.yaml + +- tapOn: "Discover" +- extendedWaitUntil: { visible: { id: discovery_screen }, timeout: 8000 } +- tapOn: "Create Your Own Currency" +- extendedWaitUntil: { visible: { id: currency_creator_screen }, timeout: 8000 } +- assertVisible: "Create Your Currency" + +# The $20 creation fee exceeds the test account's giveable balance, so Get Started +# surfaces the add-money gate. +- tapOn: "Get Started" +- assertVisible: "Add More Money" diff --git a/maestro/sell.yaml b/maestro/sell.yaml new file mode 100644 index 000000000..7eb3fbfce --- /dev/null +++ b/maestro/sell.yaml @@ -0,0 +1,17 @@ +appId: com.flipcash.app.android +name: "Sell — token info → amount entry (fund-safe)" +tags: + - smoke + - swap +--- +# Fund-safe: reaches the sell amount-entry. Reaching the sell confirmation depends on +# the held balance clearing the minimum + fees, so it's left for a funded account. +- runFlow: subflows/login_with_deeplink.yaml +- runFlow: subflows/navigate_to_wallet.yaml + +- tapOn: "Float" +- extendedWaitUntil: { visible: { id: token_info_screen }, timeout: 8000 } +- tapOn: "Sell" +- extendedWaitUntil: { visible: { id: swap_screen }, timeout: 8000 } +- assertVisible: "Amount to Sell" +- assertVisible: { id: keypad_5 } From 86a4d29cc9e45110b5cdcdc1d5d584b1eac6acb1 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 16:03:47 -0400 Subject: [PATCH 12/19] docs(maestro): mark send-to-contact & full Coinbase purchase as provisioning-blocked Note how iOS handles each: send-to-contact runs against a phone-provisioned account; the Coinbase purchase isn't automated on iOS either (E2E stops at verification, order logic is unit-tested), so our onramp-entry flow is already at parity. --- maestro/README.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/maestro/README.md b/maestro/README.md index 5eebb3fae..e69e2ba7f 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -123,13 +123,20 @@ BETA_FLAGS=tipping_enabled maestro/run.sh maestro/tipping_setup.yaml `coinbase_onramp_sandbox_enabled` set so a follow-up can drive a sandbox purchase - Give/bill round-trip, token-info deeplink, screenshot suite (existing) -**Roadmap / not-yet-covered:** -- **Full Coinbase sandbox purchase** — the flow reaches the onramp; completing a purchase needs - phone verification first (which links a phone to the shared account — a side effect that would - change the send flows) and driving the Google Pay sandbox sheet. Sandbox flag + method tag are - in place. -- **Send-to-contact** (send cash + message to a Flipcash contact) — needs a phone-linked account - with contacts; `send_contact_list`/`send_contact_row` are tagged and ready. +**Provisioning-blocked** (need a differently-provisioned test account, not more tooling): +- **Send-to-contact** (send cash + message to a Flipcash contact) — needs a **phone-linked account + with contacts**. iOS covers this in `SendSmokeTests` by running against a real funded account that + *has* a verified phone/contacts; ours is intentionally phone-less (which is why `direct_send` stops + at the phone gate). `send_contact_list`/`send_contact_row` are tagged and ready for such an account. +- **Full Coinbase purchase** — the flow reaches the onramp; completing it needs phone verification + (which links a phone to the shared account and would flip the send flows) plus driving the Google + Pay sandbox sheet. Note: **iOS doesn't automate the payment either** — its E2E stops at the same + onramp/verification boundary (`BuyApplePayRegressionTests`: unverified → verification sheet) and + covers order-building/deposit/verification logic with unit tests (`OnrampOrderRequestTests`, + `CoinbaseDepositOperationTests`, `OnrampVerificationViewModelTests`). So our `coinbase_onramp` entry + test is at parity; the sandbox flag + method tag are in place if we later want to go further. + +**Roadmap (tooling):** - Buy/Sell/Withdraw past confirmation on a funded account (screens tagged). - Wire a `flipcash_maestro` Fastlane lane (see below). From aaeebc197a5b56981449aaddc986bfdac306e555 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 16:16:33 -0400 Subject: [PATCH 13/19] test(maestro): scaffold USDF-only gate + send-to-contact flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - login_usdf_only.yaml + usdf_only_gate.yaml: deeplink login into a dedicated reserves-only account and assert the give→Discover gate (mirrors iOS GiveDiscoverGateRegressionTests). Needs USDF_ONLY_DEEPLINK. - send_to_contact.yaml: send to an on-Flipcash contact (mirrors iOS SendSmokeTests). Parameterized by CONTACT_NAME/CONTACT_PHONE; needs a phone-linked account + the contact seeded in the emulator. - run.sh forwards USDF_ONLY_DEEPLINK / CONTACT_NAME / CONTACT_PHONE; README documents the new .env keys and marks both flows as provisioning-pending scaffolds. --- maestro/README.md | 23 ++++++++++++++++------- maestro/run.sh | 8 ++++++++ maestro/send_to_contact.yaml | 27 +++++++++++++++++++++++++++ maestro/subflows/login_usdf_only.yaml | 14 ++++++++++++++ maestro/usdf_only_gate.yaml | 19 +++++++++++++++++++ 5 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 maestro/send_to_contact.yaml create mode 100644 maestro/subflows/login_usdf_only.yaml create mode 100644 maestro/usdf_only_gate.yaml diff --git a/maestro/README.md b/maestro/README.md index e69e2ba7f..7e91d40d6 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -17,9 +17,14 @@ iOS's `FlipcashUITests`. Flows are plain YAML under `maestro/`; reusable pieces 3. The [Maestro CLI](https://maestro.mobile.dev) on your `PATH` (`maestro --version`). 4. Test-account credentials in `maestro/.env` (git-ignored): ``` - SEED_PHRASE=word1 word2 ... word12 - LOGIN_DEEPLINK=https://app.flipcash.com/login?data=... + SEED_PHRASE=word1 word2 ... word12 # primary account (tip-enabled) + LOGIN_DEEPLINK=https://app.flipcash.com/login?data=... # same account as SEED_PHRASE + TIPCARD_DEEPLINK=https://app.flipcash.com/tip/... # the primary account's tip card + USDF_ONLY_DEEPLINK=https://app.flipcash.com/login?data=... # reserves-only gate account + CONTACT_NAME=Brandon McAnsh # an on-Flipcash contact for send-to-contact + CONTACT_PHONE=+15869802333 # seed this contact into the emulator ``` + The runner (`run.sh`) forwards all of these to Maestro. ## Running @@ -123,11 +128,15 @@ BETA_FLAGS=tipping_enabled maestro/run.sh maestro/tipping_setup.yaml `coinbase_onramp_sandbox_enabled` set so a follow-up can drive a sandbox purchase - Give/bill round-trip, token-info deeplink, screenshot suite (existing) -**Provisioning-blocked** (need a differently-provisioned test account, not more tooling): -- **Send-to-contact** (send cash + message to a Flipcash contact) — needs a **phone-linked account - with contacts**. iOS covers this in `SendSmokeTests` by running against a real funded account that - *has* a verified phone/contacts; ours is intentionally phone-less (which is why `direct_send` stops - at the phone gate). `send_contact_list`/`send_contact_row` are tagged and ready for such an account. +**Scaffolded — pending account provisioning** (flow authored + wired; drop in the account/contact +and it runs): +- `usdf_only_gate.yaml` — reserves-only account: tapping Cash routes to Discover ("No Community + Currencies Yet"). Mirrors iOS `GiveDiscoverGateRegressionTests`. Needs `USDF_ONLY_DEEPLINK` + (a dedicated USDF-only account, like iOS's `FLIPCASH_UI_TEST_USDF_ONLY_ACCESS_KEY`). +- `send_to_contact.yaml` — send to an on-Flipcash contact (mirrors iOS `SendSmokeTests`, which uses a + fixed contact "Raul Riera"). Needs a **send-enabled** account (a phone linked — use the backend test + number `+15005550000`/`000000`, or a real number with `adb emu sms send`) **and** the `CONTACT_NAME` + contact seeded in the emulator as a real Flipcash user. Parameterized by `CONTACT_NAME`/`CONTACT_PHONE`. - **Full Coinbase purchase** — the flow reaches the onramp; completing it needs phone verification (which links a phone to the shared account and would flip the send flows) plus driving the Google Pay sandbox sheet. Note: **iOS doesn't automate the payment either** — its E2E stops at the same diff --git a/maestro/run.sh b/maestro/run.sh index 0991bde69..46cf4a932 100755 --- a/maestro/run.sh +++ b/maestro/run.sh @@ -28,6 +28,11 @@ fi SEED_PHRASE="$(grep '^SEED_PHRASE=' "$ENV_FILE" | cut -d= -f2-)" LOGIN_DEEPLINK="$(grep '^LOGIN_DEEPLINK=' "$ENV_FILE" | cut -d= -f2-)" TIPCARD_DEEPLINK="$(grep '^TIPCARD_DEEPLINK=' "$ENV_FILE" | cut -d= -f2-)" +# Dedicated USDF-only (reserves-only) account for gate tests. +USDF_ONLY_DEEPLINK="$(grep '^USDF_ONLY_DEEPLINK=' "$ENV_FILE" | cut -d= -f2-)" +# On-Flipcash contact for send-to-contact tests (seeded into the emulator's contacts). +CONTACT_NAME="$(grep '^CONTACT_NAME=' "$ENV_FILE" | cut -d= -f2-)" +CONTACT_PHONE="$(grep '^CONTACT_PHONE=' "$ENV_FILE" | cut -d= -f2-)" # App Links verification does not survive a fresh install; approve so # https://app.flipcash.com/... deeplinks open the app, not Chrome. @@ -42,5 +47,8 @@ maestro --device "$DEVICE" test \ -e SEED_PHRASE="$SEED_PHRASE" \ -e LOGIN_DEEPLINK="$LOGIN_DEEPLINK" \ -e TIPCARD_DEEPLINK="$TIPCARD_DEEPLINK" \ + -e USDF_ONLY_DEEPLINK="$USDF_ONLY_DEEPLINK" \ + -e CONTACT_NAME="$CONTACT_NAME" \ + -e CONTACT_PHONE="$CONTACT_PHONE" \ -e BETA_FLAGS="${BETA_FLAGS:-}" \ "$@" diff --git a/maestro/send_to_contact.yaml b/maestro/send_to_contact.yaml new file mode 100644 index 000000000..5465a79de --- /dev/null +++ b/maestro/send_to_contact.yaml @@ -0,0 +1,27 @@ +appId: com.flipcash.app.android +name: "Send to a Flipcash contact" +tags: + - payments +--- +# Send cash to an on-Flipcash contact, then land in the conversation. Mirrors iOS +# SendSmokeTests (which sends to a fixed contact, "Raul Riera"). +# +# Requires: +# - a send-enabled account (a phone linked), else the flow stops at the phone gate; +# - the contact CONTACT_NAME (CONTACT_PHONE) seeded in the emulator's contacts, and that +# contact being a real Flipcash user. +# Until both are provisioned this is a scaffold — the steps encode the expected journey. +- runFlow: subflows/login_with_deeplink.yaml + +- tapOn: "Send" +- extendedWaitUntil: { visible: { id: send_screen }, timeout: 8000 } + +# With a linked phone the send flow shows the contact list (not the phone gate). +- extendedWaitUntil: { visible: { id: send_contact_list }, timeout: 8000 } +- tapOn: { id: send_search_field } +- inputText: ${CONTACT_NAME} +- extendedWaitUntil: { visible: { id: send_contact_row }, timeout: 8000 } +- tapOn: { id: send_contact_row, index: 0 } + +# Opening a Flipcash contact lands in the conversation (from which cash/amount is entered). +- extendedWaitUntil: { visible: { id: chat_screen }, timeout: 8000 } diff --git a/maestro/subflows/login_usdf_only.yaml b/maestro/subflows/login_usdf_only.yaml new file mode 100644 index 000000000..3973cdbf0 --- /dev/null +++ b/maestro/subflows/login_usdf_only.yaml @@ -0,0 +1,14 @@ +# Deeplink login into the dedicated USDF-only test account (reserves only, no community +# currency) used for gate tests. Requires env: USDF_ONLY_DEEPLINK. +appId: com.flipcash.app.android +--- +- stopApp +- runFlow: + file: ../helpers/launch_deeplink.yaml + env: + clearAppState: "true" + deeplink: ${USDF_ONLY_DEEPLINK} + +- extendedWaitUntil: + visible: + id: "scanner_screen" diff --git a/maestro/usdf_only_gate.yaml b/maestro/usdf_only_gate.yaml new file mode 100644 index 000000000..bfcc883f6 --- /dev/null +++ b/maestro/usdf_only_gate.yaml @@ -0,0 +1,19 @@ +appId: com.flipcash.app.android +name: "Gate — USDF-only account: give routes to Discover" +tags: + - gate +--- +# Deterministic gate coverage on a reserves-only account (holds USDF, no community +# currency). Mirrors iOS GiveDiscoverGateRegressionTests: with a balance but nothing +# giveable, tapping Cash surfaces "No Community Currencies Yet" and routes to Discover. +# +# Requires env: USDF_ONLY_DEEPLINK (a provisioned USDF-only test account). Until that +# account exists this flow is a scaffold — the assertions encode the expected behaviour. +- runFlow: subflows/login_usdf_only.yaml + +- tapOn: "Cash" +- extendedWaitUntil: + visible: + text: "No Community Currencies Yet" + timeout: 8000 +- assertVisible: "Discover Currencies" From 36d36a38bbe8cc94f3b132ef49e050c8cd199369 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 16:22:48 -0400 Subject: [PATCH 14/19] test(maestro): auto-seed the send-to-contact recipient in the runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run.sh now seeds CONTACT_NAME/CONTACT_PHONE into the emulator's contacts before running flows (idempotent; skips if already present). Verified: wiping contacts and running the runner re-creates the contact. Removes a manual setup step for send-to-contact — only a send-enabled (phone-linked) account remains. --- maestro/README.md | 7 ++++--- maestro/run.sh | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/maestro/README.md b/maestro/README.md index 7e91d40d6..07df19671 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -134,9 +134,10 @@ and it runs): Currencies Yet"). Mirrors iOS `GiveDiscoverGateRegressionTests`. Needs `USDF_ONLY_DEEPLINK` (a dedicated USDF-only account, like iOS's `FLIPCASH_UI_TEST_USDF_ONLY_ACCESS_KEY`). - `send_to_contact.yaml` — send to an on-Flipcash contact (mirrors iOS `SendSmokeTests`, which uses a - fixed contact "Raul Riera"). Needs a **send-enabled** account (a phone linked — use the backend test - number `+15005550000`/`000000`, or a real number with `adb emu sms send`) **and** the `CONTACT_NAME` - contact seeded in the emulator as a real Flipcash user. Parameterized by `CONTACT_NAME`/`CONTACT_PHONE`. + fixed contact "Raul Riera"). Parameterized by `CONTACT_NAME`/`CONTACT_PHONE`; **the runner seeds this + contact into the emulator automatically** (idempotent). The only remaining requirement is a + **send-enabled** account (a phone linked — use the backend test number `+15005550000`/`000000`, or a + real number with `adb emu sms send`), and that `CONTACT_PHONE` maps to a real Flipcash user. - **Full Coinbase purchase** — the flow reaches the onramp; completing it needs phone verification (which links a phone to the shared account and would flip the send flows) plus driving the Google Pay sandbox sheet. Note: **iOS doesn't automate the payment either** — its E2E stops at the same diff --git a/maestro/run.sh b/maestro/run.sh index 46cf4a932..393620837 100755 --- a/maestro/run.sh +++ b/maestro/run.sh @@ -38,6 +38,31 @@ CONTACT_PHONE="$(grep '^CONTACT_PHONE=' "$ENV_FILE" | cut -d= -f2-)" # https://app.flipcash.com/... deeplinks open the app, not Chrome. adb -s "$DEVICE" shell pm set-app-links --package "$APP_ID" 2 all >/dev/null 2>&1 || true +# Seed the send-to-contact recipient into the emulator's contacts (idempotent). No-op +# unless CONTACT_NAME + CONTACT_PHONE are set. The device-side single quotes preserve +# spaces in the name; the new raw contact is the highest auto-increment _id. +seed_contact() { + [[ -z "$CONTACT_NAME" || -z "$CONTACT_PHONE" ]] && return 0 + local data_uri="content://com.android.contacts/data" + local raw_uri="content://com.android.contacts/raw_contacts" + if adb -s "$DEVICE" shell content query --uri "$data_uri" --projection mimetype:data1 2>/dev/null \ + | grep -q "$CONTACT_PHONE"; then + return 0 # already seeded + fi + adb -s "$DEVICE" shell content insert --uri "$raw_uri" \ + --bind account_name:s: --bind account_type:s: >/dev/null 2>&1 + local rid + rid=$(adb -s "$DEVICE" shell content query --uri "$raw_uri" --projection _id 2>/dev/null \ + | grep -oE '_id=[0-9]+' | cut -d= -f2 | sort -n | tail -1) + [[ -z "$rid" ]] && { echo "warning: could not seed contact" >&2; return 0; } + adb -s "$DEVICE" shell "content insert --uri $data_uri --bind raw_contact_id:i:$rid \ + --bind mimetype:s:vnd.android.cursor.item/name --bind data1:s:'$CONTACT_NAME'" >/dev/null 2>&1 + adb -s "$DEVICE" shell "content insert --uri $data_uri --bind raw_contact_id:i:$rid \ + --bind mimetype:s:vnd.android.cursor.item/phone_v2 --bind data1:s:'$CONTACT_PHONE' \ + --bind data2:i:2" >/dev/null 2>&1 +} +seed_contact + if [[ $# -eq 0 ]]; then echo "usage: $0 [more flows...]" >&2 exit 1 From dac6b3f91417b85a56cd60cb3ec045132a4b6a44 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 16:25:23 -0400 Subject: [PATCH 15/19] docs(maestro): distinguish onboarding vs send-flow phone verification Onboarding/account creation uses the backend test number (+15005550000 / 000000); linking a phone to enable the send flow must use the emulator's real number + real SMS (adb emu sms send), since the test number isn't a linkable identity for send-to-contact. --- maestro/README.md | 13 +++++++++++-- maestro/send_to_contact.yaml | 11 ++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/maestro/README.md b/maestro/README.md index 07df19671..318ea7a92 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -136,8 +136,17 @@ and it runs): - `send_to_contact.yaml` — send to an on-Flipcash contact (mirrors iOS `SendSmokeTests`, which uses a fixed contact "Raul Riera"). Parameterized by `CONTACT_NAME`/`CONTACT_PHONE`; **the runner seeds this contact into the emulator automatically** (idempotent). The only remaining requirement is a - **send-enabled** account (a phone linked — use the backend test number `+15005550000`/`000000`, or a - real number with `adb emu sms send`), and that `CONTACT_PHONE` maps to a real Flipcash user. + **send-enabled account** — i.e. a phone linked to the account (see below), and `CONTACT_PHONE` mapping + to a real Flipcash user. + +### Two phone-verification paths + +- **Onboarding / account creation** uses the **backend test number** `+15005550000` with OTP `000000` + (`create_account.yaml`). This is a backend test hook — no real SMS, no linkable identity. +- **Linking a phone to enable the send flow** must use the **emulator's real number + real SMS**, since + the fake test number isn't a linkable identity for send-to-contact. Enter the number in the link-phone + screen, then deliver the code to the emulator with `adb emu sms send ` (or the emulator + console). This is the path a `send_to_contact` run needs to establish once, per account. - **Full Coinbase purchase** — the flow reaches the onramp; completing it needs phone verification (which links a phone to the shared account and would flip the send flows) plus driving the Google Pay sandbox sheet. Note: **iOS doesn't automate the payment either** — its E2E stops at the same diff --git a/maestro/send_to_contact.yaml b/maestro/send_to_contact.yaml index 5465a79de..863f0a419 100644 --- a/maestro/send_to_contact.yaml +++ b/maestro/send_to_contact.yaml @@ -6,11 +6,12 @@ tags: # Send cash to an on-Flipcash contact, then land in the conversation. Mirrors iOS # SendSmokeTests (which sends to a fixed contact, "Raul Riera"). # -# Requires: -# - a send-enabled account (a phone linked), else the flow stops at the phone gate; -# - the contact CONTACT_NAME (CONTACT_PHONE) seeded in the emulator's contacts, and that -# contact being a real Flipcash user. -# Until both are provisioned this is a scaffold — the steps encode the expected journey. +# Requires (see README "Two phone-verification paths"): +# - a send-enabled account: a phone linked via the emulator's real number + real SMS +# (`adb emu sms send`), not the onboarding test number — else the flow stops at the phone gate; +# - the CONTACT_NAME / CONTACT_PHONE contact (a real Flipcash user) in the emulator's contacts — +# the runner seeds this automatically. +# Until the account is phone-linked this is a scaffold — the steps encode the expected journey. - runFlow: subflows/login_with_deeplink.yaml - tapOn: "Send" From f4bf984618e73dd96ea6f7fdd66609328b2c1877 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 16:28:19 -0400 Subject: [PATCH 16/19] docs(maestro): note app SMS auto-extraction + one-time phone link for send The app auto-extracts the SMS code, so linking a phone for the send flow just needs the code delivered to the emulator (adb emu sms send). It's a one-time per-account provisioning step, not a per-run action. --- maestro/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/maestro/README.md b/maestro/README.md index 318ea7a92..cc9fbd411 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -144,9 +144,15 @@ and it runs): - **Onboarding / account creation** uses the **backend test number** `+15005550000` with OTP `000000` (`create_account.yaml`). This is a backend test hook — no real SMS, no linkable identity. - **Linking a phone to enable the send flow** must use the **emulator's real number + real SMS**, since - the fake test number isn't a linkable identity for send-to-contact. Enter the number in the link-phone - screen, then deliver the code to the emulator with `adb emu sms send ` (or the emulator - console). This is the path a `send_to_contact` run needs to establish once, per account. + the fake test number isn't a linkable identity for send-to-contact. The backend sends the code to the + number, and the app has **SMS auto-extraction**, so the code just needs to arrive at the emulator: + ``` + # in the link-phone screen: enter the number, request the code, then deliver it — + adb emu sms send "Your Flipcash code is 123456" + ``` + The app reads the code from the injected SMS and completes the link. This is **one-time per account** + (the phone stays linked), so it's a provisioning step, not something a `send_to_contact` run repeats — + once a dedicated send account is phone-linked, `send_to_contact.yaml` runs green (contact auto-seeded). - **Full Coinbase purchase** — the flow reaches the onramp; completing it needs phone verification (which links a phone to the shared account and would flip the send flows) plus driving the Google Pay sandbox sheet. Note: **iOS doesn't automate the payment either** — its E2E stops at the same From 85ba074c6d746cc3808156eb42eed945129f58a9 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 17:04:57 -0400 Subject: [PATCH 17/19] docs(maestro): record phone-link findings (SMS auto-read works; real code delivery blocks) Verified: valid-format number accepted (emulator 555 number is invalid); adb emu sms send + Android SMS User Consent auto-fills the code; but the backend requires the real code, which is sent to the entered number and never reaches the emulator. Unblock = route the verification SMS to the emulator (backend webhook -> adb emu sms send). --- maestro/README.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/maestro/README.md b/maestro/README.md index cc9fbd411..194145410 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -143,16 +143,19 @@ and it runs): - **Onboarding / account creation** uses the **backend test number** `+15005550000` with OTP `000000` (`create_account.yaml`). This is a backend test hook — no real SMS, no linkable identity. -- **Linking a phone to enable the send flow** must use the **emulator's real number + real SMS**, since - the fake test number isn't a linkable identity for send-to-contact. The backend sends the code to the - number, and the app has **SMS auto-extraction**, so the code just needs to arrive at the emulator: - ``` - # in the link-phone screen: enter the number, request the code, then deliver it — - adb emu sms send "Your Flipcash code is 123456" - ``` - The app reads the code from the injected SMS and completes the link. This is **one-time per account** - (the phone stays linked), so it's a provisioning step, not something a `send_to_contact` run repeats — - once a dedicated send account is phone-linked, `send_to_contact.yaml` runs green (contact auto-seeded). +- **Linking a phone to enable the send flow** — status: **blocked on code delivery.** What's verified: + - A valid-format number is required (the emulator's own `555-521-5554` is an invalid NPA and is + rejected at phone entry). A number like `+1 415-555-0100` is accepted and the code is requested. + - The app uses Android's **SMS User Consent** reader: an SMS delivered via + `adb emu sms send "…code is 123456"` lands in the inbox and the app prompts to read it and + auto-fills the code. **This path works.** + - **But the backend validates the real code** — an injected placeholder is rejected + ("Please enter a valid code"). The real code is sent to the entered number, which does **not** route + to the emulator, so it never arrives and can't be read. + - **To unblock:** the dev/staging backend must route the verification SMS for the test number **to this + emulator** (e.g. a webhook that calls `adb emu sms send`), so the real code lands in the inbox and the + app reads it. Once that exists, phone-linking is one-time per account and `send_to_contact.yaml` runs + green (contact is auto-seeded by the runner). - **Full Coinbase purchase** — the flow reaches the onramp; completing it needs phone verification (which links a phone to the shared account and would flip the send flows) plus driving the Google Pay sandbox sheet. Note: **iOS doesn't automate the payment either** — its E2E stops at the same From aa9096ba4a7f5c3f1b983916636b5e4ba4970cad Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 17:23:14 -0400 Subject: [PATCH 18/19] ci(maestro): add flipcash_maestro lane + emulator workflow - run.sh: env-var-first creds (CI secrets) with maestro/.env fallback, optional .env, and a `--tags ` mode that runs the suite tag-filtered with JUnit output. - Fastfile: flipcash_maestro lane installs the debug build and runs run.sh in tag mode (MAESTRO_TAGS default smoke, MAESTRO_EXCLUDE_TAGS default spends-funds). - .github/workflows/maestro.yml: boots a KVM x86_64 emulator, sets up build secrets, installs the Maestro CLI, runs the lane, uploads the JUnit report. Triggers: workflow_dispatch (choose tags) + nightly schedule; PR-gating opt-in. - README: document the lane, workflow, local usage, and required GitHub secrets. Verified locally: `run.sh --tags gate` runs the tagged flow and emits maestro-report.xml. --- .github/workflows/maestro.yml | 104 ++++++++++++++++++++++++++++++++++ fastlane/Fastfile | 11 ++++ maestro/README.md | 25 +++++++- maestro/run.sh | 52 ++++++++++------- 4 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/maestro.yml diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml new file mode 100644 index 000000000..b343a3dc0 --- /dev/null +++ b/.github/workflows/maestro.yml @@ -0,0 +1,104 @@ +name: Maestro E2E + +# Real-backend E2E against the shared test account, so it is not wired to every PR by +# default: run it on demand (choose tags) and nightly (smoke). To gate PRs, add a +# `pull_request:` trigger below — note it needs the test-account secrets, so it won't run +# on fork PRs. +on: + workflow_dispatch: + inputs: + tags: + description: "Maestro include-tags (e.g. smoke, tipping, gate)" + default: "smoke" + exclude_tags: + description: "Maestro exclude-tags" + default: "spends-funds" + schedule: + - cron: "37 7 * * *" # nightly, off the top of the hour + +concurrency: + group: maestro-${{ github.ref }} + cancel-in-progress: true + +jobs: + maestro: + name: Maestro E2E (${{ github.event.inputs.tags || 'smoke' }}) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Java env + uses: actions/setup-java@v3 + with: + java-version: "21" + distribution: "corretto" + cache: "gradle" + + - name: Setup Ruby env + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7.2 + bundler-cache: true + + # Build prerequisites (same as the unit-test CI job). + - name: Decode Google Services JSON file + uses: timheuer/base64-to-file@v1 + with: + fileName: google-services.json + fileDir: ./apps/flipcash/app/src + encodedString: ${{ secrets.FLIPCASH2_GOOGLE_SERVICES }} + - name: Setup local.properties API keys + run: | + { + echo "BUGSNAG_API_KEY=\"${{ secrets.FLIPCASH_BUGSNAG_API_KEY }}\"" + echo "MIXPANEL_API_KEY=\"${{ secrets.FLIPCASH_MIXPANEL_API_KEY }}\"" + echo "COINBASE_ONRAMP_API_KEY=${{ secrets.COINBASE_ONRAMP_API_KEY }}" + echo "GOOGLE_CLOUD_PROJECT_NUMBER=${{ secrets.GOOGLE_CLOUD_PROJECT_NUMBER }}" + } >> ./local.properties + + - name: Install Maestro CLI + run: | + curl -fsSL "https://get.maestro.mobile.dev" | bash + echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" + + # KVM is required for a fast x86_64 emulator on GitHub-hosted Linux runners. + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ + | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Run Maestro suite on emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 34 + arch: x86_64 + profile: pixel_6 + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim + disable-animations: true + script: bundle exec fastlane android flipcash_maestro + env: + MAESTRO_TAGS: ${{ github.event.inputs.tags || 'smoke' }} + MAESTRO_EXCLUDE_TAGS: ${{ github.event.inputs.exclude_tags || 'spends-funds' }} + # Test-account credentials (map GitHub secrets -> the env vars run.sh reads). + SEED_PHRASE: ${{ secrets.MAESTRO_SEED_PHRASE }} + LOGIN_DEEPLINK: ${{ secrets.MAESTRO_LOGIN_DEEPLINK }} + TIPCARD_DEEPLINK: ${{ secrets.MAESTRO_TIPCARD_DEEPLINK }} + USDF_ONLY_DEEPLINK: ${{ secrets.MAESTRO_USDF_ONLY_DEEPLINK }} + CONTACT_NAME: ${{ secrets.MAESTRO_CONTACT_NAME }} + CONTACT_PHONE: ${{ secrets.MAESTRO_CONTACT_PHONE }} + + - name: Upload Maestro report + if: always() + uses: actions/upload-artifact@v4 + with: + name: maestro-report + path: | + maestro-report.xml + ~/.maestro/tests/** + if-no-files-found: ignore diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0631e879c..c3ee5072e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -26,6 +26,17 @@ platform :android do ) end + desc "Run the Maestro E2E suite on a booted emulator (default tag: smoke)" + # Installs the debug build, then runs maestro/run.sh in tag mode. Test-account + # creds come from the environment (SEED_PHRASE, LOGIN_DEEPLINK, TIPCARD_DEEPLINK, + # USDF_ONLY_DEEPLINK, CONTACT_NAME, CONTACT_PHONE) — supplied by CI secrets or + # maestro/.env locally. Filter with MAESTRO_TAGS / MAESTRO_EXCLUDE_TAGS. + lane :flipcash_maestro do + gradle(task: ":apps:flipcash:app:installDebug") + tags = ENV.fetch("MAESTRO_TAGS", "smoke") + sh("cd .. && maestro/run.sh --tags #{tags.shellescape}") + end + desc "Build a new version of Flipcash" lane :build_flipcash do gradle( diff --git a/maestro/README.md b/maestro/README.md index 194145410..d284d63dd 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -170,6 +170,25 @@ and it runs): ## CI -Not yet wired. Intended: a `flipcash_maestro` Fastlane lane running -`--include-tags smoke --exclude-tags spends-funds` on a KVM emulator per PR, with the fuller -set nightly. See `docs/superpowers/plans/2026-07-01-maestro-mcp-ui-testing.md`. +Wired via the **`flipcash_maestro`** Fastlane lane and the **`.github/workflows/maestro.yml`** +workflow: + +- The lane installs the debug build and runs `maestro/run.sh --tags ` (default + `smoke`, excluding `spends-funds`), emitting a JUnit report. +- The workflow boots a KVM `x86_64` emulator (`reactivecircus/android-emulator-runner`), sets up + the same build secrets as the unit-test job, installs the Maestro CLI, runs the lane, and uploads + the report. +- Triggers: **`workflow_dispatch`** (choose `tags`/`exclude_tags`) and a **nightly schedule** + (smoke). It's real-backend E2E against the shared account, so it's deliberately not on every PR; + add a `pull_request:` trigger to gate PRs (won't run on fork PRs, which lack secrets). + +Run locally the same way CI does: +```bash +MAESTRO_TAGS=smoke maestro/run.sh --tags smoke +``` + +**Required GitHub secrets** (test-account creds — the workflow maps them to the env vars +`run.sh` reads): `MAESTRO_SEED_PHRASE`, `MAESTRO_LOGIN_DEEPLINK`, `MAESTRO_TIPCARD_DEEPLINK`, +`MAESTRO_USDF_ONLY_DEEPLINK`, `MAESTRO_CONTACT_NAME`, `MAESTRO_CONTACT_PHONE` — plus the existing +build secrets (`FLIPCASH2_GOOGLE_SERVICES`, `FLIPCASH_BUGSNAG_API_KEY`, `FLIPCASH_MIXPANEL_API_KEY`, +`COINBASE_ONRAMP_API_KEY`, `GOOGLE_CLOUD_PROJECT_NUMBER`). diff --git a/maestro/run.sh b/maestro/run.sh index 393620837..469f650f1 100755 --- a/maestro/run.sh +++ b/maestro/run.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash -# Convenience runner for the Maestro E2E suite. +# Convenience runner for the Maestro E2E suite (local and CI). # # Usage: -# maestro/run.sh [more flows...] -# maestro/run.sh maestro/account_navigation.yaml +# maestro/run.sh [more flows...] # run specific flows (local dev) +# maestro/run.sh --tags smoke # run by tag, JUnit output (CI) # -# Handles the fiddly setup that a fresh emulator/install needs: -# - loads SEED_PHRASE / LOGIN_DEEPLINK from maestro/.env (values may contain spaces) +# Handles the fiddly setup a fresh emulator/install needs: +# - loads creds from maestro/.env when present; existing env vars win (CI supplies them) # - approves App Links so https deeplinks route to the app instead of the browser +# - seeds the send-to-contact recipient into the emulator's contacts (idempotent) # - targets a specific device when several are attached (DEVICE env, default emulator-5554) # # Prereqs (see maestro/README.md): emulator booted, debug app installed @@ -19,20 +20,20 @@ APP_ID="com.flipcash.app.android" DEVICE="${DEVICE:-emulator-5554}" ENV_FILE="$SCRIPT_DIR/.env" -if [[ ! -f "$ENV_FILE" ]]; then - echo "error: $ENV_FILE not found (needs SEED_PHRASE and LOGIN_DEEPLINK)." >&2 - exit 1 -fi - -# Load creds without word-splitting the space-containing seed phrase. -SEED_PHRASE="$(grep '^SEED_PHRASE=' "$ENV_FILE" | cut -d= -f2-)" -LOGIN_DEEPLINK="$(grep '^LOGIN_DEEPLINK=' "$ENV_FILE" | cut -d= -f2-)" -TIPCARD_DEEPLINK="$(grep '^TIPCARD_DEEPLINK=' "$ENV_FILE" | cut -d= -f2-)" +# Value from the current environment (CI secrets) if set, else from maestro/.env. +cred() { + local name="$1" current="${!1:-}" + if [[ -n "$current" ]]; then printf '%s' "$current"; return; fi + [[ -f "$ENV_FILE" ]] && grep "^${name}=" "$ENV_FILE" | cut -d= -f2- || true +} +SEED_PHRASE="$(cred SEED_PHRASE)" +LOGIN_DEEPLINK="$(cred LOGIN_DEEPLINK)" +TIPCARD_DEEPLINK="$(cred TIPCARD_DEEPLINK)" # Dedicated USDF-only (reserves-only) account for gate tests. -USDF_ONLY_DEEPLINK="$(grep '^USDF_ONLY_DEEPLINK=' "$ENV_FILE" | cut -d= -f2-)" +USDF_ONLY_DEEPLINK="$(cred USDF_ONLY_DEEPLINK)" # On-Flipcash contact for send-to-contact tests (seeded into the emulator's contacts). -CONTACT_NAME="$(grep '^CONTACT_NAME=' "$ENV_FILE" | cut -d= -f2-)" -CONTACT_PHONE="$(grep '^CONTACT_PHONE=' "$ENV_FILE" | cut -d= -f2-)" +CONTACT_NAME="$(cred CONTACT_NAME)" +CONTACT_PHONE="$(cred CONTACT_PHONE)" # App Links verification does not survive a fresh install; approve so # https://app.flipcash.com/... deeplinks open the app, not Chrome. @@ -63,9 +64,20 @@ seed_contact() { } seed_contact -if [[ $# -eq 0 ]]; then - echo "usage: $0 [more flows...]" >&2 +# Build the maestro target: `--tags ` runs the whole suite filtered by tag with +# JUnit output (CI); otherwise the args are treated as specific flow files (local dev). +if [[ "${1:-}" == "--tags" ]]; then + shift + include="${1:-smoke}" + target=( --include-tags "$include" + --exclude-tags "${MAESTRO_EXCLUDE_TAGS:-spends-funds}" + --format junit --output "${MAESTRO_OUTPUT:-maestro-report.xml}" + "$SCRIPT_DIR" ) +elif [[ $# -eq 0 ]]; then + echo "usage: $0 [more flows...] | $0 --tags " >&2 exit 1 +else + target=( "$@" ) fi maestro --device "$DEVICE" test \ @@ -76,4 +88,4 @@ maestro --device "$DEVICE" test \ -e CONTACT_NAME="$CONTACT_NAME" \ -e CONTACT_PHONE="$CONTACT_PHONE" \ -e BETA_FLAGS="${BETA_FLAGS:-}" \ - "$@" + "${target[@]}" From 21dfa49692e36773702ca1bec1f9f7026bce8ec2 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 31 Jul 2026 17:26:08 -0400 Subject: [PATCH 19/19] ci(maestro): exclude account-creating flows from default runs Tag tipping_setup (the only flow that onboards a new account) with `creates-account` and exclude `spends-funds,creates-account` by default in run.sh and the workflow, so the smoke/nightly run never has side effects. `smoke` was already free of account-creating flows; this makes it explicit and robust. Run such flows deliberately by clearing the exclude (MAESTRO_EXCLUDE_TAGS=). --- .github/workflows/maestro.yml | 6 +++--- maestro/README.md | 7 ++++++- maestro/run.sh | 2 +- maestro/tipping_setup.yaml | 1 + 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index b343a3dc0..6c8fd6003 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -11,8 +11,8 @@ on: description: "Maestro include-tags (e.g. smoke, tipping, gate)" default: "smoke" exclude_tags: - description: "Maestro exclude-tags" - default: "spends-funds" + description: "Maestro exclude-tags (side-effecting flows excluded by default)" + default: "spends-funds,creates-account" schedule: - cron: "37 7 * * *" # nightly, off the top of the hour @@ -84,7 +84,7 @@ jobs: script: bundle exec fastlane android flipcash_maestro env: MAESTRO_TAGS: ${{ github.event.inputs.tags || 'smoke' }} - MAESTRO_EXCLUDE_TAGS: ${{ github.event.inputs.exclude_tags || 'spends-funds' }} + MAESTRO_EXCLUDE_TAGS: ${{ github.event.inputs.exclude_tags || 'spends-funds,creates-account' }} # Test-account credentials (map GitHub secrets -> the env vars run.sh reads). SEED_PHRASE: ${{ secrets.MAESTRO_SEED_PHRASE }} LOGIN_DEEPLINK: ${{ secrets.MAESTRO_LOGIN_DEEPLINK }} diff --git a/maestro/README.md b/maestro/README.md index d284d63dd..763874582 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -174,7 +174,12 @@ Wired via the **`flipcash_maestro`** Fastlane lane and the **`.github/workflows/ workflow: - The lane installs the debug build and runs `maestro/run.sh --tags ` (default - `smoke`, excluding `spends-funds`), emitting a JUnit report. + `smoke`, excluding `spends-funds,creates-account`), emitting a JUnit report. + +Side-effecting flows are tagged so runs stay clean: `spends-funds` (moves money) and +`creates-account` (onboards a new account, e.g. `tipping_setup.yaml`) are **excluded by +default**. `smoke` contains only read-only / fund-safe navigation. To run an account-creating +flow deliberately, clear the exclude, e.g. `MAESTRO_TAGS=tipping MAESTRO_EXCLUDE_TAGS= maestro/run.sh --tags tipping`. - The workflow boots a KVM `x86_64` emulator (`reactivecircus/android-emulator-runner`), sets up the same build secrets as the unit-test job, installs the Maestro CLI, runs the lane, and uploads the report. diff --git a/maestro/run.sh b/maestro/run.sh index 469f650f1..447cfab7b 100755 --- a/maestro/run.sh +++ b/maestro/run.sh @@ -70,7 +70,7 @@ if [[ "${1:-}" == "--tags" ]]; then shift include="${1:-smoke}" target=( --include-tags "$include" - --exclude-tags "${MAESTRO_EXCLUDE_TAGS:-spends-funds}" + --exclude-tags "${MAESTRO_EXCLUDE_TAGS:-spends-funds,creates-account}" --format junit --output "${MAESTRO_OUTPUT:-maestro-report.xml}" "$SCRIPT_DIR" ) elif [[ $# -eq 0 ]]; then diff --git a/maestro/tipping_setup.yaml b/maestro/tipping_setup.yaml index 80cb63350..24fc3d135 100644 --- a/maestro/tipping_setup.yaml +++ b/maestro/tipping_setup.yaml @@ -2,6 +2,7 @@ appId: com.flipcash.app.android name: "Tipping — create account & set up tip card" tags: - tipping + - creates-account --- # Tip setup is one-run-per-account, so start from a brand-new account with the tipping # beta flag enabled at launch (no Labs-UI toggling).