From 35e5d2199f63f95c7582c4593dd7ff36585221dd Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 17 Sep 2026 07:50:06 -0300 Subject: [PATCH 1/7] fix: avoid animated switch to auto tab Co-Authored-By: Claude Opus 5 (1M context) --- .../wallets/receive/ReceiveQrScreen.kt | 43 +++++++++++--- .../receive/ReceiveAutoTabSwitchTest.kt | 57 +++++++++++++++++++ changelog.d/next/876.fixed.md | 1 + 3 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveAutoTabSwitchTest.kt create mode 100644 changelog.d/next/876.fixed.md diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 5a9e04890b..f54bd86c4b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.gestures.snapping.SnapPosition import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -188,6 +189,7 @@ fun ReceiveQrScreen( } var hasAppliedInitialTab by remember { mutableStateOf(false) } var appliedInitialTab by remember { mutableStateOf(null) } + var hasUserSelectedTab by remember { mutableStateOf(false) } LaunchedEffect(visibleTabs, initialTab) { val requestedTab = initialTab?.takeIf { it in visibleTabs } @@ -227,18 +229,28 @@ fun ReceiveQrScreen( } } - // Auto-switch to AUTO tab when it becomes available for the first time - LaunchedEffect(canCreateLightningInvoice, cjitInvoice) { - val shouldAutoSwitch = initialTab == null && canCreateLightningInvoice && cjitInvoice.isNullOrEmpty() - if (shouldAutoSwitch && visibleTabs.contains(ReceiveTab.AUTO)) { - val autoIndex = visibleTabs.indexOf(ReceiveTab.AUTO) - if (autoIndex != -1) { - lazyListState.animateScrollToItem(autoIndex) - selectedTab = ReceiveTab.AUTO - } + LaunchedEffect(lazyListState) { + lazyListState.interactionSource.interactions.collect { + if (it is DragInteraction.Start) hasUserSelectedTab = true } } + // Jump to AUTO tab without animation when it becomes available before the user picks a tab + LaunchedEffect(canCreateLightningInvoice, cjitInvoice) { + val shouldAutoSwitch = shouldAutoSwitchToAuto( + selectedTab = selectedTab, + hasUserSelectedTab = hasUserSelectedTab, + canCreateLightningInvoice = canCreateLightningInvoice, + cjitInvoice = cjitInvoice, + initialTab = initialTab, + ) + if (!shouldAutoSwitch) return@LaunchedEffect + val autoIndex = visibleTabs.indexOf(ReceiveTab.AUTO) + if (autoIndex == -1 || lazyListState.firstVisibleItemIndex == autoIndex) return@LaunchedEffect + lazyListState.scrollToItem(autoIndex) + selectedTab = ReceiveTab.AUTO + } + // Auto-switch to Spending tab when CJIT is not null LaunchedEffect(cjitInvoice) { if (cjitInvoice != null) { @@ -301,6 +313,7 @@ fun ReceiveQrScreen( onTabChange = { tab -> haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) val newIndex = visibleTabs.indexOf(tab) + hasUserSelectedTab = true selectedTab = tab showDetails = false scope.launch { @@ -478,6 +491,18 @@ private fun List.defaultReceiveTab(): ReceiveTab { return if (contains(ReceiveTab.AUTO)) ReceiveTab.AUTO else ReceiveTab.SAVINGS } +internal fun shouldAutoSwitchToAuto( + selectedTab: ReceiveTab, + hasUserSelectedTab: Boolean, + canCreateLightningInvoice: Boolean, + cjitInvoice: String?, + initialTab: ReceiveTab?, +): Boolean { + if (initialTab != null || hasUserSelectedTab) return false + if (selectedTab == ReceiveTab.AUTO) return false + return canCreateLightningInvoice && cjitInvoice.isNullOrEmpty() +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ReceiveQrView( diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveAutoTabSwitchTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveAutoTabSwitchTest.kt new file mode 100644 index 0000000000..c6d7bdcbbe --- /dev/null +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveAutoTabSwitchTest.kt @@ -0,0 +1,57 @@ +package to.bitkit.ui.screens.wallets.receive + +import org.junit.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ReceiveAutoTabSwitchTest { + + @Test + fun `switches when lightning becomes available and user has not selected a tab`() { + assertTrue(decide()) + } + + @Test + fun `does not switch when auto is already selected`() { + assertFalse(decide(selectedTab = ReceiveTab.AUTO)) + } + + @Test + fun `does not switch after user selected a tab`() { + assertFalse(decide(hasUserSelectedTab = true)) + } + + @Test + fun `does not switch when lightning invoice cannot be created`() { + assertFalse(decide(canCreateLightningInvoice = false)) + } + + @Test + fun `does not switch when cjit invoice exists`() { + assertFalse(decide(cjitInvoice = "lnbcrt1cjit")) + } + + @Test + fun `switches when cjit invoice is empty`() { + assertTrue(decide(cjitInvoice = "")) + } + + @Test + fun `does not switch when an initial tab was requested`() { + assertFalse(decide(initialTab = ReceiveTab.SPENDING)) + } + + private fun decide( + selectedTab: ReceiveTab = ReceiveTab.SAVINGS, + hasUserSelectedTab: Boolean = false, + canCreateLightningInvoice: Boolean = true, + cjitInvoice: String? = null, + initialTab: ReceiveTab? = null, + ) = shouldAutoSwitchToAuto( + selectedTab = selectedTab, + hasUserSelectedTab = hasUserSelectedTab, + canCreateLightningInvoice = canCreateLightningInvoice, + cjitInvoice = cjitInvoice, + initialTab = initialTab, + ) +} diff --git a/changelog.d/next/876.fixed.md b/changelog.d/next/876.fixed.md new file mode 100644 index 0000000000..839d2144d0 --- /dev/null +++ b/changelog.d/next/876.fixed.md @@ -0,0 +1 @@ +Fixed the Receive sheet sliding to the Auto tab on open and overriding a tab you had already picked. From beae20379b9c569543dac02bf4b10febc031b3a4 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 17 Sep 2026 11:34:35 -0300 Subject: [PATCH 2/7] fix: clear stale receive amount before opening sheet Co-Authored-By: Claude Opus 5 (1M context) --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 2 ++ .../viewmodels/AppViewModelSendFlowTest.kt | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index c912066536..914896aca9 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -4723,6 +4723,8 @@ class AppViewModel @Inject constructor( onchainAddress = walletRepo.getOnchainAddress(), ) } + // A stale amount above inbound liquidity would hide the Auto tab until the receive state refreshes + if (sheetType is Sheet.Receive) walletRepo.setBip21AmountSats(null) _currentSheet.update { sheetType } } sheetTransitionJob = nextJob diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index 829cd2bf29..6ebf7254d0 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -4135,6 +4135,28 @@ class AppViewModelSendFlowTest : BaseUnitTest() { } } + @Test + fun `showSheet clears stale receive amount before presenting the receive sheet`() = test { + var sheetWhenCleared: Sheet? = Sheet.Send() + whenever(walletRepo.setBip21AmountSats(null)).thenAnswer { sheetWhenCleared = sut.currentSheet.value } + + val sheet = Sheet.Receive() + sut.showSheet(sheet) + advanceUntilIdle() + + verify(walletRepo).setBip21AmountSats(null) + assertNull(sheetWhenCleared) + assertEquals(sheet, sut.currentSheet.value) + } + + @Test + fun `showSheet keeps receive amount when presenting another sheet`() = test { + sut.showSheet(Sheet.Send()) + advanceUntilIdle() + + verify(walletRepo, never()).setBip21AmountSats(anyOrNull()) + } + @Test fun `received lightning payment closes the active receive sheet after wallet invoice is cleared`() = test { walletState.value = WalletState(bolt11 = "settled-invoice") From 47fdb148122614d184d7ad384c1c48a9c2ac0e44 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 17 Sep 2026 13:00:58 -0300 Subject: [PATCH 3/7] chore: rename changelog fragment Co-Authored-By: Claude Opus 5 (1M context) --- changelog.d/next/{876.fixed.md => 1308.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{876.fixed.md => 1308.fixed.md} (100%) diff --git a/changelog.d/next/876.fixed.md b/changelog.d/next/1308.fixed.md similarity index 100% rename from changelog.d/next/876.fixed.md rename to changelog.d/next/1308.fixed.md From fbc1dc281d2f0612ca539546f2a65406a3a6cc2b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 17 Sep 2026 20:21:22 -0300 Subject: [PATCH 4/7] test: cover receive auto tab wiring in compose Co-Authored-By: Claude Opus 5 (1M context) --- .../receive/ReceiveAutoTabSelectionTest.kt | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 app/src/androidTest/java/to/bitkit/ui/screens/wallets/receive/ReceiveAutoTabSelectionTest.kt diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/receive/ReceiveAutoTabSelectionTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/receive/ReceiveAutoTabSelectionTest.kt new file mode 100644 index 0000000000..afc4135b2e --- /dev/null +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/receive/ReceiveAutoTabSelectionTest.kt @@ -0,0 +1,99 @@ +package to.bitkit.ui.screens.wallets.receive + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import kotlinx.collections.immutable.persistentListOf +import org.junit.Rule +import org.junit.Test +import to.bitkit.ext.createChannelDetails +import to.bitkit.models.NodeLifecycleState +import to.bitkit.repositories.LightningState +import to.bitkit.repositories.WalletState +import to.bitkit.test.annotations.ComposeUi +import to.bitkit.ui.theme.AppThemeSurface +import kotlin.test.assertEquals + +@ComposeUi +class ReceiveAutoTabSelectionTest { + @get:Rule + val composeTestRule = createComposeRule() + + private var lightningState by mutableStateOf(STATE_WITHOUT_INBOUND) + private val editedTabs = mutableListOf() + + @Test + fun keepsTabPickedByTapWhenAutoBecomesAvailable() { + setContent() + + composeTestRule.onNodeWithTag("Tab-spending").performClick() + composeTestRule.onNodeWithTag("Tab-savings").performClick() + composeTestRule.waitForIdle() + + makeAutoAvailable() + + assertEquals(ReceiveTab.SAVINGS, selectedTab()) + } + + @Test + fun jumpsToAutoWhenNoTabWasPicked() { + setContent() + composeTestRule.waitForIdle() + + makeAutoAvailable() + + assertEquals(ReceiveTab.AUTO, selectedTab()) + } + + private fun setContent() { + composeTestRule.setContent { + AppThemeSurface { + ReceiveQrScreen( + cjitInvoice = null, + walletState = WALLET_STATE, + lightningState = lightningState, + onClickEditInvoice = { editedTabs += it }, + onClickReceiveCjit = {}, + ) + } + } + } + + private fun makeAutoAvailable() { + composeTestRule.runOnIdle { lightningState = STATE_WITH_INBOUND } + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("Tab-auto").assertIsDisplayed() + } + + private fun selectedTab(): ReceiveTab { + composeTestRule.onAllNodesWithTag("SpecifyInvoiceButton")[0].performClick() + composeTestRule.waitForIdle() + return editedTabs.last() + } + + private companion object { + const val ADDRESS = "bcrt1qreceiveaddress" + const val BOLT11 = "lnbcrt1invoice" + val WALLET_STATE = WalletState( + onchainAddress = ADDRESS, + bolt11 = BOLT11, + bip21 = "bitcoin:$ADDRESS?lightning=$BOLT11", + ) + val STATE_WITHOUT_INBOUND = LightningState(nodeLifecycleState = NodeLifecycleState.Running) + val STATE_WITH_INBOUND = LightningState( + nodeLifecycleState = NodeLifecycleState.Running, + channels = persistentListOf( + createChannelDetails().copy( + isChannelReady = true, + isUsable = true, + inboundCapacityMsat = 100_000_000u, + ) + ), + ) + } +} From 7ae8457ea5ee077150e762e75404da1cc49915ce Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 18 Sep 2026 10:33:42 -0300 Subject: [PATCH 5/7] docs: add receive auto tab selection journey Co-Authored-By: Claude Opus 5 (1M context) --- journeys/README.md | 2 + .../receive/receive-auto-tab-selection.xml | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 journeys/receive/receive-auto-tab-selection.xml diff --git a/journeys/README.md b/journeys/README.md index 403c5e2309..19e191346e 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -121,6 +121,7 @@ fixtures, push notifications) live in each suite's README. | [notification-permission](notification-permission) | 4 | Background-setup toggles | | [payment-requests](payment-requests) | 2 | Requires a linked fixture issuer; rejected shapes are unit fixtures | | [pubky-marketplace](pubky-marketplace) | 1 | Two-wallet Paykit marketplace payment; integration fixture required | +| [receive](receive) | 1 | Receive sheet tab selection; needs a spending channel, no README | | [widgets](widgets) | 2 | Needs no backend — the quickest way to see the loop work; no README | ## Cross-platform @@ -142,6 +143,7 @@ Known differences in the corpus, as of the iOS port (synonymdev/bitkit-ios#691): | `hardware-wallet/receive-onchain.xml`, `hardware-wallet/send-onchain.xml` | not ported | | `payment-requests/requested-resolution-failure.xml` | not ported | | `deeplinks/*` | not ported — iOS registers the `bitkit` scheme but has no screen or sheet router | +| `receive/receive-auto-tab-selection.xml` | not ported — the Auto tab override fix is Android-only so far; iOS parity not checked | | — | `hardware-wallet/transfer-to-spending-over-max.xml` exists only on iOS | ### Running one on iOS diff --git a/journeys/receive/receive-auto-tab-selection.xml b/journeys/receive/receive-auto-tab-selection.xml new file mode 100644 index 0000000000..53ed5b9e03 --- /dev/null +++ b/journeys/receive/receive-auto-tab-selection.xml @@ -0,0 +1,57 @@ + + + Verifies that the Receive sheet opens on the Auto tab without sliding to it, that a tab the user + picks is not overridden by the Auto tab, and that a leftover invoice amount from an earlier Edit + Invoice session does not open the sheet on Savings. Requires a wallet with a usable spending + channel so the Auto tab is available once the Lightning node is running. The selected tab is + shown only by its underline, which `android layout` does not expose, so tab assertions need a + screenshot; the Auto tab's presence can be read from the testTag "Tab-auto". + + + + Launch the Bitkit app and go to the wallet home screen + + + Tap the Receive button (testTag "Receive"), take a screenshot within one second, and verify the + Auto tab (testTag "Tab-auto") is selected on the first frame of the sheet + + + Swipe the QR code area from left to right once and verify the Savings tab (testTag + "Tab-savings") is selected + + + Wait 3 seconds, take a screenshot and verify the Savings tab is still selected + + + Close the sheet, then run `adb shell am force-stop to.bitkit.dev` and launch the app again + + + As soon as the home screen shows, tap the Receive button and verify the tab row has no Auto tab + (testTag "Tab-auto" absent) while the Lightning node starts + + + Tap the Trezor tab (testTag "Tab-trezor") if present, then tap the Savings tab (testTag + "Tab-savings") + + + Wait until the Auto tab (testTag "Tab-auto") appears, wait 5 more seconds, take a screenshot and + verify the Savings tab is still selected + + + Close the sheet, force-stop and relaunch the app, wait for the node to start, and tap the Receive + button without touching the tabs; verify the Savings tab is selected first and that, once the + Auto tab appears, the sheet jumps to Auto without a sliding animation (record the screen to + check) + + + Tap "Edit" (testTag "SpecifyInvoiceButton"), tap the amount field (testTag + "ReceiveNumberPadTextField"), enter an amount above the inbound Lightning capacity (for example + N3 N0 N0 N0 in USD), tap Continue (testTag "ReceiveNumberPadSubmit") and then "QR Code" (testTag + "ShowQrReceive"); verify the Auto tab (testTag "Tab-auto") is absent + + + Close the sheet, tap the Receive button again and verify the Auto tab (testTag "Tab-auto") is + present and selected on the first frame of the sheet + + + From 049c57fdf6f0080ec829fb09d8557d92703005a0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 18 Sep 2026 11:36:46 -0300 Subject: [PATCH 6/7] docs: make auto tab journey steps prove the fix Co-Authored-By: Claude Opus 5 (1M context) --- .../receive/receive-auto-tab-selection.xml | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/journeys/receive/receive-auto-tab-selection.xml b/journeys/receive/receive-auto-tab-selection.xml index 53ed5b9e03..11b86e3685 100644 --- a/journeys/receive/receive-auto-tab-selection.xml +++ b/journeys/receive/receive-auto-tab-selection.xml @@ -5,7 +5,9 @@ Invoice session does not open the sheet on Savings. Requires a wallet with a usable spending channel so the Auto tab is available once the Lightning node is running. The selected tab is shown only by its underline, which `android layout` does not expose, so tab assertions need a - screenshot; the Auto tab's presence can be read from the testTag "Tab-auto". + screenshot; the Auto tab's presence can be read from the testTag "Tab-auto". The underline itself + crossfades over 200 ms even when the switch is instant, so the two steps that check for an + animation read the QR pager out of a screen recording instead of the underline. @@ -38,10 +40,16 @@ verify the Savings tab is still selected - Close the sheet, force-stop and relaunch the app, wait for the node to start, and tap the Receive - button without touching the tabs; verify the Savings tab is selected first and that, once the - Auto tab appears, the sheet jumps to Auto without a sliding animation (record the screen to - check) + Close the sheet, force-stop and relaunch the app, and start `adb shell screenrecord + --time-limit 20 /sdcard/receive-auto.mp4`. As soon as the home screen shows, tap the Receive + button without touching the tabs and verify the Auto tab (testTag "Tab-auto") is absent; do not + wait for the node, the tab row must still be Savings-first when the sheet opens + + + Wait until the Auto tab (testTag "Tab-auto") appears, let the recording finish, pull it (`adb + pull /sdcard/receive-auto.mp4`), extract frames (`ffmpeg -i receive-auto.mp4 -vf fps=30 + frames/%03d.png`) and verify that no frame shows the QR pager part-way between the Savings and + the Auto page: the sheet jumps to Auto in one frame instead of sliding Tap "Edit" (testTag "SpecifyInvoiceButton"), tap the amount field (testTag @@ -50,8 +58,15 @@ "ShowQrReceive"); verify the Auto tab (testTag "Tab-auto") is absent - Close the sheet, tap the Receive button again and verify the Auto tab (testTag "Tab-auto") is - present and selected on the first frame of the sheet + Start `adb shell screenrecord --time-limit 20 /sdcard/receive-reopen.mp4`, close the sheet and + tap the Receive button again, then verify the Auto tab (testTag "Tab-auto") is present + + + Let the recording finish, pull it (`adb pull /sdcard/receive-reopen.mp4`), extract frames + (`ffmpeg -i receive-reopen.mp4 -vf fps=30 frames/%03d.png`) and verify that the first frame in + which the sheet is visible already shows the Auto tab in the tab row with the selected + underline, and that no frame shows Savings selected. A sheet that opens on Savings and only + then gains the Auto tab means the leftover invoice amount was not cleared From c5b4923534e356ef7dff7c7701e8386c87ac7d75 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 18 Sep 2026 11:59:30 -0300 Subject: [PATCH 7/7] docs: require the auto tab jump on tape to pass Co-Authored-By: Claude Opus 5 (1M context) --- .../receive/receive-auto-tab-selection.xml | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/journeys/receive/receive-auto-tab-selection.xml b/journeys/receive/receive-auto-tab-selection.xml index 11b86e3685..c70f767362 100644 --- a/journeys/receive/receive-auto-tab-selection.xml +++ b/journeys/receive/receive-auto-tab-selection.xml @@ -7,7 +7,10 @@ shown only by its underline, which `android layout` does not expose, so tab assertions need a screenshot; the Auto tab's presence can be read from the testTag "Tab-auto". The underline itself crossfades over 200 ms even when the switch is instant, so the two steps that check for an - animation read the QR pager out of a screen recording instead of the underline. + animation read the QR pager out of a screen recording instead of the underline. Each of those + steps first requires the recording to contain the frames it reasons about; a recording that ended + before the node started, or before the sheet opened, is a rerun with a longer `--time-limit`, not + a pass. @@ -46,10 +49,21 @@ wait for the node, the tab row must still be Savings-first when the sheet opens - Wait until the Auto tab (testTag "Tab-auto") appears, let the recording finish, pull it (`adb - pull /sdcard/receive-auto.mp4`), extract frames (`ffmpeg -i receive-auto.mp4 -vf fps=30 - frames/%03d.png`) and verify that no frame shows the QR pager part-way between the Savings and - the Auto page: the sheet jumps to Auto in one frame instead of sliding + Wait until the Auto tab (testTag "Tab-auto") appears, and check that it appeared while the + recording was still running. The node has to finish `lightningService.start` and load its + channels before the Auto tab exists, which on a cold process can outlast the 20 second window; + if the Auto tab is still absent when the recording ends, the jump happened off-tape and the run + holds no evidence, so repeat the previous step with a longer `--time-limit` rather than reading + the empty recording as a pass + + + Let the recording finish, pull it (`adb pull /sdcard/receive-auto.mp4`), extract frames (`ffmpeg + -i receive-auto.mp4 -vf fps=30 frames/%03d.png`) and verify first that the recording contains + the jump at all: a frame showing the Savings page centred in the QR pager, followed by a later + frame showing the Auto page centred. If either frame is missing, the step did not capture the + switch and has to be rerun; it is not a pass. Then verify that no frame between those two shows + the QR pager part-way between the Savings and the Auto page: the sheet jumps to Auto in one + frame instead of sliding Tap "Edit" (testTag "SpecifyInvoiceButton"), tap the amount field (testTag @@ -63,10 +77,11 @@ Let the recording finish, pull it (`adb pull /sdcard/receive-reopen.mp4`), extract frames - (`ffmpeg -i receive-reopen.mp4 -vf fps=30 frames/%03d.png`) and verify that the first frame in - which the sheet is visible already shows the Auto tab in the tab row with the selected - underline, and that no frame shows Savings selected. A sheet that opens on Savings and only - then gains the Auto tab means the leftover invoice amount was not cleared + (`ffmpeg -i receive-reopen.mp4 -vf fps=30 frames/%03d.png`) and verify that the recording holds + frames in which the sheet is visible — if it holds none, rerun the step, it is not a pass — then + that the first such frame already shows the Auto tab in the tab row with the selected underline, + and that no frame shows Savings selected. A sheet that opens on Savings and only then gains the + Auto tab means the leftover invoice amount was not cleared