LockScreenRoutingTest.kt
1package com.peachpasswords.ui.lock23import com.peachpasswords.ui.PeachNav4import org.junit.Assert.assertEquals5import org.junit.Assert.assertTrue6import org.junit.Test78/**9 * Pins the unlock-routing decision in [routeOnUnlock]. The LaunchedEffect body in10 * [LockScreen] is a thin dispatcher over this function, so the priority order11 * (created > autofill > postUnlock) and the lazy `postUnlock` semantics are the12 * only thing worth locking down here.13 *14 * Background: a previous v2.14.5 guard put the autofill-pending branch inside15 * LockScreen and was keyed on both `state.unlocked` and `autofillPending`. The16 * NavHost's autofill LaunchedEffect consumed `newEntryRequest` synchronously,17 * which flipped `autofillPending` to false mid-unlock and re-fired LockScreen's18 * effect — landing the user on the vault list instead of the new-entry editor.19 * The fix keyed LockScreen's effect on `state.unlocked` only; these tests pin20 * the decision so a future reorder can't silently re-introduce the regression.21 */22class LockScreenRoutingTest {2324 private fun state(unlocked: Boolean = true, created: Boolean = false) =25 LockUiState(unlocked = unlocked, created = created)2627 @Test fun created_vault_always_routes_to_recovery_setup_regardless_of_autofill() {28 // A freshly created vault must walk through recovery-word setup before29 // the list — even if an autofill nav request is queued, the user just30 // made the vault and the recovery phrase is the immediate priority.31 val dest = PeachNav.VaultList // any sentinel; must not be consulted32 assertEquals(33 UnlockRoute.RecoverySetup,34 routeOnUnlock(state(created = true), autofillPending = false) { dest },35 )36 assertEquals(37 UnlockRoute.RecoverySetup,38 routeOnUnlock(state(created = true), autofillPending = true) { dest },39 )40 }4142 @Test fun autofill_pending_unlock_defers_to_the_navhost() {43 // The regression: when autofillPending was true at the unlock transition,44 // LockScreen must NOT route to the post-unlock destination or the NavHost's45 // editor navigation gets clobbered with popUpTo(0).46 var postUnlockCalls = 047 val route = routeOnUnlock(state(created = false), autofillPending = true) {48 postUnlockCalls++49 PeachNav.VaultList50 }51 assertEquals(UnlockRoute.Defer, route)52 assertEquals(53 "Defer must not consult postUnlock — lazy lookup matters when the " +54 "destination is expensive (lastViewed → resolve → firstActive)",55 0, postUnlockCalls,56 )57 }5859 @Test fun ordinary_unlock_routes_through_the_viewmodel_destination() {60 // The base case: a normal unlock lands on the destination the ViewModel61 // computed (last-viewed entry, or the vault list, or recovery setup).62 val expected = PeachNav.EntryDetail("abc-123")63 val route = routeOnUnlock(state(created = false), autofillPending = false) { expected }64 assertTrue(route is UnlockRoute.PostUnlock)65 assertEquals(expected, (route as UnlockRoute.PostUnlock).destination)66 }6768 @Test fun postUnlock_lookup_runs_exactly_once_on_ordinary_unlock() {69 // Sanity: postUnlock is invoked exactly once per ordinary unlock so the70 // ViewModel-side resolution work (which can be non-trivial) is bounded.71 var calls = 072 val route = routeOnUnlock(state(created = false), autofillPending = false) {73 calls++74 PeachNav.VaultList75 }76 assertTrue(route is UnlockRoute.PostUnlock)77 assertEquals(1, calls)78 }79}