Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e9c2733
test(maestro): fix stale appId across the suite
bmc08gt Jul 31, 2026
6455b62
feat(navigation): apply screen-root testTags centrally via annotatedE…
bmc08gt Jul 31, 2026
e6ce57e
test(maestro): add verified feature flows, runner, and README
bmc08gt Jul 31, 2026
1a66992
test(maestro): tag flow steps centrally; repair logout via My Account
bmc08gt Jul 31, 2026
82c68c3
test(maestro): add send/discovery/withdraw/deposit flows; repair drif…
bmc08gt Jul 31, 2026
a61560b
feat(featureflags): enable beta flags via launch arg for UI tests
bmc08gt Jul 31, 2026
4b89aa9
test(maestro): add tip-chat flow (open conversation + send message)
bmc08gt Jul 31, 2026
634e729
test(maestro): add blocking flow (block + unblock a chat participant)
bmc08gt Jul 31, 2026
d15535e
test(maestro): add tip-deeplink flow (opens the tip flow)
bmc08gt Jul 31, 2026
674f40c
feat(funding): tag purchase-method buttons for UI tests
bmc08gt Jul 31, 2026
bccacb2
test(maestro): add buy, sell, currency-creator, coinbase-onramp flows
bmc08gt Jul 31, 2026
86a4d29
docs(maestro): mark send-to-contact & full Coinbase purchase as provi…
bmc08gt Jul 31, 2026
aaeebc1
test(maestro): scaffold USDF-only gate + send-to-contact flows
bmc08gt Jul 31, 2026
36d36a3
test(maestro): auto-seed the send-to-contact recipient in the runner
bmc08gt Jul 31, 2026
dac6b3f
docs(maestro): distinguish onboarding vs send-flow phone verification
bmc08gt Jul 31, 2026
f4bf984
docs(maestro): note app SMS auto-extraction + one-time phone link for…
bmc08gt Jul 31, 2026
85ba074
docs(maestro): record phone-link findings (SMS auto-read works; real …
bmc08gt Jul 31, 2026
aa9096b
ci(maestro): add flipcash_maestro lane + emulator workflow
bmc08gt Jul 31, 2026
21dfa49
ci(maestro): exclude account-creating flows from default runs
bmc08gt Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/workflows/maestro.yml
Original file line number Diff line number Diff line change
@@ -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 (side-effecting flows excluded by default)"
default: "spends-funds,creates-account"
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,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 }}
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
28 changes: 28 additions & 0 deletions apps/flipcash/app/src/main/kotlin/com/flipcash/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ fun appEntryProvider(
annotatedEntry<AppRoute.Main.InviteContact> { key -> InviteContactScreen(key.phoneNumber) }

// Sheets (inner content — wrapped in Main.Sheet by navigateTo())
annotatedEntry<AppRoute.Sheets.Give> { key -> CashScreen(key.mint, key.fromTokenInfo) }
// Route type is `Give` but the screen is the Cash/Give screen the flows call cash_screen.
annotatedEntry<AppRoute.Sheets.Give>(testTag = "cash_screen") { key -> CashScreen(key.mint, key.fromTokenInfo) }
annotatedEntry<AppRoute.Sheets.Send> { SendFlowScreen(resultStateRegistry = resultStateRegistry) }
annotatedEntry<AppRoute.Sheets.Tips> { key ->
TippingFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
Expand All @@ -109,10 +110,12 @@ fun appEntryProvider(
}

// Tokens
annotatedEntry<AppRoute.Token.Info> { key ->
annotatedEntry<AppRoute.Token.Info>(testTag = "token_info_screen") { key ->
TokenInfoScreen(key.mint, key.shortfall, key.fromDeeplink)
}
annotatedEntry<AppRoute.Token.Transactions> { key -> TransactionHistoryScreen(key.mint) }
annotatedEntry<AppRoute.Token.Transactions>(testTag = "transaction_history_screen") { key ->
TransactionHistoryScreen(key.mint)
}
annotatedEntry<AppRoute.Token.Swap> { key ->
SwapFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,9 @@ internal fun resolvePostAccountRoute(
private fun onboardingEntryProvider(
route: AppRoute.OnboardingFlow,
): (NavKey) -> NavEntry<NavKey> = entryProvider {
annotatedEntry<OnboardingStep.Start> { step ->
// The pre-login landing; flows/screenshots anchor on `login_screen` rather than
// the step-derived `start_screen`.
annotatedEntry<OnboardingStep.Start>(testTag = "login_screen") { step ->
LoginStepContent(step.seed)
}
annotatedEntry<OnboardingStep.SeedInput> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,7 +83,6 @@ private fun TokenInfoScreen(
LazyColumn(
modifier = Modifier
.fillMaxSize()
.testTag("token_info_screen")
.padding(
start = innerPadding.calculateStartPadding(),
end = innerPadding.calculateEndPadding(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,15 @@ private fun withdrawalEntryProvider(
annotatedEntry<WithdrawalStep.SelectToken> {
WithdrawalSelectTokenScreen()
}
annotatedEntry<WithdrawalStep.Amount> { 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<WithdrawalStep.Amount>(testTag = "withdraw_entry_screen") { step ->
WithdrawalEntryScreen(step.mint)
}
annotatedEntry<WithdrawalStep.Destination> {
annotatedEntry<WithdrawalStep.Destination>(testTag = "withdraw_destination_screen") {
WithdrawalDestinationScreen()
}
annotatedEntry<WithdrawalStep.Confirmation> {
annotatedEntry<WithdrawalStep.Confirmation>(testTag = "withdraw_confirmation_screen") {
WithdrawalConfirmationScreen()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ internal fun purchaseOptions(
width = 150.sp,
height = 20.sp,
tintIcon = false,
testTag = "purchase_method_coinbase",
onClick = { onClick(PurchaseMethod.CoinbaseOnRamp) }
)
)
Expand All @@ -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) }
)
)
Expand All @@ -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) }
)
)
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,24 @@ 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(
text: String,
style: BottomBarManager.BottomBarButtonStyle = BottomBarManager.BottomBarButtonStyle.Filled,
isUser: Boolean = true,
enabled: Boolean = true,
testTag: String? = null,
onClick: () -> Unit = { }
) : this(
text = AnnotatedString(text),
inlineContentMap = emptyMap(),
style = style,
isUser = isUser,
enabled = enabled,
testTag = testTag,
onClick = onClick
)

Expand Down
Loading
Loading