test(send): Phase A characterization tests for SendFormViewModel#4377
Conversation
Adds a 42-test safety net before splitting the 3415-LOC SendFormViewModel into focused sub-components. These lock down the public surface that SendFormScreen and its section composables depend on, so the upcoming extractions cannot regress observable behavior unnoticed. - SendFormViewModelInitTest (14): loadData arg parsing, vault name, fiat currency, expandedSection rules, slippage seed for REDEEM_YRUNE. - SendFormViewModelAddressTest (11): setOutputAddress, setProviderAddress, isDstAddressComplete flow, scanAddress / scanProviderAddress, setAddressFromQrCode, openAddressBook no-token gate. - SendFormViewModelAmountTest (13): validateTokenAmount branches, toggleAmountInputType, expandSection, dismissError, back, chooseMaxTokenAmount, choosePercentageAmount F25/F50/F75/F100. - SendFormViewModelSubmitEvmTest (4): send() validation gates and onClickContinue dispatch. No production code changes. Co-Authored-By: aminsato <Amin.saradar@yahoo.com>
📝 WalkthroughWalkthroughAdds four coroutine-based unit test suites for SendFormViewModel (address, amount, init, EVM submit). Each test file configures a test Main dispatcher, statically mocks SavedStateHandle route-to-Route.Send, stubs currency/vault flows, and provides a buildViewModel() test helper. ChangesSendFormViewModel Unit Tests
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 4/5 reviews remaining, refill in 12 minutes. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAddressTest.kt (1)
199-215: ⚡ Quick winRename this test to match the asserted no-op behavior.
The current name says address is routed into
addressFieldState, but the assertion verifies it remains empty when no token is selected.Suggested rename
- fun `openAddressBook OUTPUT routes the address into addressFieldState`() = runTest { + fun `openAddressBook OUTPUT is a no-op when selectedToken is missing`() = runTest {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAddressTest.kt` around lines 199 - 215, The test name is inaccurate: rename the test function currently named `openAddressBook OUTPUT routes the address into addressFieldState` to reflect the asserted no-op behavior when no token is selected (e.g., mention "no-op" or "keeps addressFieldState empty"); update the test declaration (the function name) that contains the calls to `buildViewModel()`, `vm.openAddressBook(AddressBookType.OUTPUT)`, and the assertion against `vm.addressFieldState.text` so the name clearly describes that the address book flow is gated on token selection and leaves the address field empty when `selectedTokenValue` is null.app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelInitTest.kt (1)
20-23: Use Kotest assertions in these new unit tests for project consistency.These tests currently use
kotlin.testassertions, while repo test guidelines require Kotest assertions inapp/src/test/**/*.kt. This pattern appears across multiple test files in this directory (SendFormViewModelInitTest.kt, SendFormViewModelTronStakingTest.kt, SendFormViewModelSubmitEvmTest.kt, SendFormViewModelAmountTest.kt, and SendFormViewModelAddressTest.kt).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelInitTest.kt` around lines 20 - 23, Tests in SendFormViewModelInitTest (and the other SendFormViewModel* test files) import and use kotlin.test assertions (assertEquals, assertFalse, assertNull, assertTrue); replace those with the Kotest assertion functions to match project conventions: update the imports to use Kotest (e.g., shouldBe, shouldBeTrue/shouldBeFalse, shouldBeNull or shouldBe) and change usages inside the tests in SendFormViewModelInitTest, SendFormViewModelTronStakingTest, SendFormViewModelSubmitEvmTest, SendFormViewModelAmountTest, and SendFormViewModelAddressTest to the corresponding Kotest matchers (keeping the same semantics) so all assertions use Kotest APIs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAddressTest.kt`:
- Around line 199-215: The test name is inaccurate: rename the test function
currently named `openAddressBook OUTPUT routes the address into
addressFieldState` to reflect the asserted no-op behavior when no token is
selected (e.g., mention "no-op" or "keeps addressFieldState empty"); update the
test declaration (the function name) that contains the calls to
`buildViewModel()`, `vm.openAddressBook(AddressBookType.OUTPUT)`, and the
assertion against `vm.addressFieldState.text` so the name clearly describes that
the address book flow is gated on token selection and leaves the address field
empty when `selectedTokenValue` is null.
In
`@app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelInitTest.kt`:
- Around line 20-23: Tests in SendFormViewModelInitTest (and the other
SendFormViewModel* test files) import and use kotlin.test assertions
(assertEquals, assertFalse, assertNull, assertTrue); replace those with the
Kotest assertion functions to match project conventions: update the imports to
use Kotest (e.g., shouldBe, shouldBeTrue/shouldBeFalse, shouldBeNull or
shouldBe) and change usages inside the tests in SendFormViewModelInitTest,
SendFormViewModelTronStakingTest, SendFormViewModelSubmitEvmTest,
SendFormViewModelAmountTest, and SendFormViewModelAddressTest to the
corresponding Kotest matchers (keeping the same semantics) so all assertions use
Kotest APIs.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b368e03b-5dfa-4f72-b5db-dfd66eb9bd1b
📒 Files selected for processing (4)
app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAddressTest.ktapp/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAmountTest.ktapp/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelInitTest.ktapp/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelSubmitEvmTest.kt
…avior Per nit on PR #4377: the test asserts that openAddressBook bails early when selectedTokenValue is null, not that the address routes through. Co-Authored-By: aminsato <Amin.saradar@yahoo.com>
|
Addressing CodeRabbit nits: Fixed (db8cd31): Renamed Deferred — Kotest assertions: CLAUDE.md does state "Use Kotest for assertions", but in practice:
Adding the Kotest dependency and migrating one PR's worth of tests would create the lone Kotest island in the repo. That's a separate codebase-wide convention decision (either migrate everything or update CLAUDE.md to match reality), out of scope for a test-only safety-net PR. Happy to file a follow-up if there's appetite to standardize. |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAddressTest.kt`:
- Around line 199-214: The test openAddressBook is a no-op when no token is
selected should additionally assert that the address-book use case is not
called: after calling vm.openAddressBook(AddressBookType.OUTPUT) and
advanceUntilIdle(), add a coroutine verify against the mocked
requestAddressBookEntry to ensure it was never invoked (e.g., coVerify {
requestAddressBookEntry.invoke(any(), any()) wasNot Called } or coVerify(exactly
= 0) { requestAddressBookEntry.invoke(any(), any()) }); this guarantees
openAddressBook returned early before consulting the use case while keeping the
existing assertions on vm.addressFieldState.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8d91ce53-d52e-4bf2-9570-5f2e35eea72b
📒 Files selected for processing (4)
app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAddressTest.ktapp/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAmountTest.ktapp/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelInitTest.ktapp/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelSubmitEvmTest.kt
Co-Authored-By: aminsato <Amin.saradar@yahoo.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAddressTest.kt (1)
42-44: ReplaceUnconfinedTestDispatcherwithStandardTestDispatcherfor deterministic test scheduling.The test suite extensively uses
advanceUntilIdle()(lines 71, 74, 82, 85, etc.) to control coroutine execution order.UnconfinedTestDispatcherbypasses task queuing and can makeadvanceUntilIdle()ineffective for scheduling control, leading to unpredictable test behavior.StandardTestDispatcher(scheduler)ensures tasks are queued on the scheduler and allowsadvanceUntilIdle()to execute them deterministically, providing more reliable test stability and aligning with the test's reliance on explicit advancement calls.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAddressTest.kt` around lines 42 - 44, Replace the non-deterministic UnconfinedTestDispatcher with a queued StandardTestDispatcher so tests that call advanceUntilIdle() behave deterministically: update the definition of mainDispatcher (which currently uses UnconfinedTestDispatcher(scheduler)) to use StandardTestDispatcher(scheduler) and ensure any test setup that sets Dispatchers.setMain(mainDispatcher) continues to use that mainDispatcher; this keeps TestCoroutineScheduler + advanceUntilIdle() semantics intact for SendFormViewModelAddressTest.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAddressTest.kt`:
- Around line 42-44: Replace the non-deterministic UnconfinedTestDispatcher with
a queued StandardTestDispatcher so tests that call advanceUntilIdle() behave
deterministically: update the definition of mainDispatcher (which currently uses
UnconfinedTestDispatcher(scheduler)) to use StandardTestDispatcher(scheduler)
and ensure any test setup that sets Dispatchers.setMain(mainDispatcher)
continues to use that mainDispatcher; this keeps TestCoroutineScheduler +
advanceUntilIdle() semantics intact for SendFormViewModelAddressTest.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9686f9f9-beba-4264-af4f-9b632e98d248
📒 Files selected for processing (1)
app/src/test/java/com/vultisig/wallet/ui/models/send/SendFormViewModelAddressTest.kt
|
@coderabbitai Skipping this one.
Switching to |
|
Tip For best results, initiate chat on the files or code changes.
✏️ Learnings added
|
…ultisig#4412) * refactor(send): split submit functions into per-defiType strategies Phase 3 of the SendFormViewModel refactor (PR 1 vultisig#4377, PR 2 vultisig#4393 already merged). Each defiType branch in SendFormViewModel now lives behind a dedicated SendSubmitStrategy implementation, so the VM no longer carries the 8 submit functions, the 5 create*DepositTransaction helpers, the inline accountValidation/awaitGasFee/getFeesFiatValue/getBitcoinTransactionPlan helpers, or the AccountValidation data class. SendFormViewModel.kt drops from 3261 → 1879 LOC. The strategies are plain classes manually constructed in the VM init block (matching the AddressManager / AmountManager precedent from PR 2). No Hilt multibinding — defiType is a closed enum and one-class-per-concern keeps the diff lift-and-shift. Public surface preserved: send() stays as a one-line passthrough so the existing screen bindings and characterization tests keep working; the four previously-private submit functions (stake/mint/redeem/withDrawUSDCCircle) and the unused public bond/unbond/ unstake passthroughs are deleted because nothing outside the VM ever called them. AccountValidator and BitcoinPlanService come out as small shared collaborators (the latter still has one VM-side caller in the collectPlanFee flow). Adds 29 Phase C tests covering AccountValidator's validate() error branches plus one error-path test per strategy (no_address, no_amount, RUNE-balance-insufficient, operator-fee out of range, slippage, WITHDRAW_RUJI silent early-return, DefaultSend pre-launch expandSection+focus). Total send.* test count: 47 Phase A → 47, Phase B managers 25 → 25, Phase C strategies 0 → 29 = 135 tests. All green via ./gradlew :app:testDebugUnitTest --tests "com.vultisig.wallet.ui.models.send.*". Also clears the IDE warnings the linter surfaced on the trimmed VM: unused BondAddress enum value, unused calculateGasLimit/enableAdvanceGasUi helpers, the cascade if-else in loadAccounts (now a when over defiType), the redundant R.string qualifier on one error-path message, and the missing @param: use-site target on chainLogo. Percentage/calculate functions (chooseMaxTokenAmount, choosePercentageAmount, calculatePercentageWithAccurateFee, fetchPercentageOfAvailableBalance) are deliberately deferred — they share the defiType switch with submit but also write to gasFee/specific and orchestrate fee recalculation, a different concern that would double the size of this PR. Co-Authored-By: aminsato <Amin.saradar@yahoo.com> * chore(send): drop unused bond/unbond/unstake VM passthroughs These three public methods previously existed only so the in-VM onClickContinue() switch could call them by name. After the strategy extraction, onClickContinue dispatches directly to the strategies and nothing outside the VM ever called bond/unbond/unstake — confirmed by grep across app/src/. Keeping send() because the existing SendFormViewModelSubmitEvmTest characterization tests still call it. Co-Authored-By: aminsato <Amin.saradar@yahoo.com> * fix(send): address CodeRabbit review on Phase 3 strategies Five fixes raised in PR vultisig#4412 review: 1. Rethrow CancellationException in every strategy submit catch block (and in the inner resolveName/resolveDstAddress catches in BondStrategy, UnbondStrategy, DefaultSendStrategy, AccountValidator). The broad catch (e: Exception) was converting normal coroutine cancellation into a spurious error toast instead of letting it propagate. 2. DefaultSendStrategy: drop the redundant first chainAccountAddressRepository .isValid check on the raw input. The check on the resolved dstAddress a few lines later already covers the validation, and the raw-input check broke the corner case where the user types an ENS/thorname and taps send before AddressManager's debounce has run — the first check would reject the name before resolveDstAddress could translate it. 3. UnstakeStrategy WITHDRAW_RUJI branch: replace the silent `?: return@launch` when the RUJI account lookup misses with `throw InvalidTransactionDataException( send_error_no_token)`. Previously tapping Continue with no RUJI account just appeared to do nothing — now the user sees an error. 4. UnstakeStrategy: wrap the getSpecific calls in createRUJIRewardsDeposit Transaction and createYTCUnstakeDepositTransaction with `withContext( Dispatchers.IO)`. Only createRUJIUnstakeDepositTransaction had it previously; the other two were running blocking repository calls on the caller dispatcher. 5. UnstakeStrategy: replace the BigInteger→Double basis-points calculation in createYTCUnstakeDepositTransaction with integer math (tokenAmountInt * 10_000 / totalTokenAmount) so token-sized values don't lose precision and emit the wrong unstake memo. Updates UnstakeStrategyTest's WITHDRAW_RUJI test from "returns silently when no RUJI account exists" to "surfaces no_token error" to match vultisig#3. All 135 send.* tests still green. Co-Authored-By: aminsato <Amin.saradar@yahoo.com> * refactor(send): address review comments on submit strategies Code: - enumerate every DeFiNavActions in onClickContinue so a future variant fails to compile instead of silently routing through DefaultSendStrategy - drop the test-only fun send() passthrough; tests use onClickContinue() - DefaultSendStrategy now goes through accountValidator.validate() and wraps getSpecific in Dispatchers.IO so the regular send path matches every other strategy - guard every strategy submit() with a Job? so a fast double-tap cannot launch two concurrent submits and persist a duplicate transaction - replace error()/RuntimeException paths in Mint/Redeem/Stake/Unstake/ WithdrawUsdcCircle with InvalidTransactionDataException so the dialog no longer leaks raw English strings or enum identifiers - bind nonDeFiAccount to a non-null val in WithdrawUsdcCircleStrategy so the later .token access is safe under any future guard tweak - swap the UiText.Empty fallback in every catch (e: Exception) for R.string.dialog_default_error_body so the dialog body is never blank Tests: - add a positive happy-path test to every strategy that reaches addTransaction(...) and asserts on the produced Transaction or DepositTransaction body — memo, dstAddress, contract address, and the ruji/tcy/ytcy/yrune contract selection inside the variant flows - de-relax the accountsRepository mock in WithdrawUsdcCircleStrategyTest and stub loadAddresses per test so future fixtures actually exercise the nonDeFiBalance < gasFee guard - mockkObject(ThorchainFunctions) and mockkObject(EthereumFunction) so the JNI-bound encoders don't require Trust Wallet Core's native library at JVM unit-test time Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: hailhydra <johnnyluo1980@gmail.com> Co-authored-by: Roman <32820910+rkokhatskyi@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
SendFormViewModel.kt(3415 LOC) before splitting it into focused sub-components.app/src/test/. All tests pass green against currentmain.AddressManager,AmountManager, and a per-chainSendDispatcherfrom the ViewModel in follow-up PRs.Why
SendFormViewModelis the largest file in the project not currently touched by any open PR. At 3415 lines with 30+ public methods and ~24 injected dependencies, it cannot be safely refactored without a behavioral baseline. These tests pin the seams that the section composables (SendFormAddressSection,SendFormAmountSection,SendFormAssetSection) andSendFormScreenrely on, so any regression during the split fails the suite loudly.What's covered
SendFormViewModelInitTest.ktloadDataarg parsing (defiType, amount, memo, slippage, autocompound),loadVaultName→srcVaultName, fiat currency fromAppCurrencyRepository, expandedSection rules frompreSelectedTokenId+addressSendFormViewModelAddressTest.ktsetOutputAddress,setProviderAddress,isDstAddressCompleteflow,scanAddress/scanProviderAddress(QR roundtrip),setAddressFromQrCode,openAddressBookno-token gateSendFormViewModelAmountTest.ktvalidateTokenAmount(4 branches: empty / zero / non-numeric / valid),toggleAmountInputType,expandSection,dismissError,back→ navigator,chooseMaxTokenAmount,choosePercentageAmountfor F25/F50/F75/F100, default fraction listSendFormViewModelSubmitEvmTest.ktsend()validation gates: blank-address focus +expandSection(Address), blank-amount focus +expandSection(Amount), no-selected-account error + no nav,onClickContinuedispatch tosend()Notable test technique
textAsFlow()uses Compose'ssnapshotFlow, which does not emit in unit tests on its own (no frame loop). Address tests that observe flow-driven state callSnapshot.sendApplyNotifications()after eachTextFieldStatemutation. This is documented in-line so future tests in this VM can copy the pattern.Out of scope (intentional)
Transactionpayload assembly →transactionRepository.addTransaction→Route.VerifySend) is not covered. Reaching it requires plumbingaccounts → preSelectToken → selectedToken → gasFeeflow — that's exactly the seam a follow-up PR will introduce, so the test is easier to write after the split rather than fighting current shape.bond/unbond/stake/unstake/mint/redeem/withDrawUSDCCircle), gas settings, and Asset/Network selection — these get their own test suites alongside the matching extraction PRs.Test plan
./gradlew :app:testDebugUnitTest --tests "com.vultisig.wallet.ui.models.send.SendFormViewModel*Test"— all 47 tests pass (42 new + 5 existingSendFormViewModelTronStakingTest)./gradlew :app:ktfmtFormat— formattedSummary by CodeRabbit