package com.craigvg.lichun_android.navigation import com.craigvg.lichun_android.ui.navigation.AchievementsKey import com.craigvg.lichun_android.ui.navigation.ActivitiesKey import com.craigvg.lichun_android.ui.navigation.ChatKey import com.craigvg.lichun_android.ui.navigation.DeathKey import com.craigvg.lichun_android.ui.navigation.HomeKey import com.craigvg.lichun_android.ui.navigation.OnboardingKey import com.craigvg.lichun_android.ui.navigation.PersonDetailKey import com.craigvg.lichun_android.ui.navigation.SocialKey import com.craigvg.lichun_android.ui.navigation.StoreKey import com.craigvg.lichun_android.ui.navigation.TOP_LEVEL_DESTINATIONS import com.craigvg.lichun_android.ui.navigation.TabbedBackStack import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config /** * Pure navigation-state logic tests for [TabbedBackStack] — the Navigation 3 * back-stack source of truth. Exercises per-tab history, tab switching, the * full-screen override stack (Onboarding/Death), `isTopLevel`/`isOverride` * derivation, `resetTo`, and back/pop semantics. * * Runs under Robolectric because the back stack is backed by Compose snapshot * state lists. */ @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE) class TabbedBackStackTest { private fun newStack() = TabbedBackStack(TOP_LEVEL_DESTINATIONS) @Test fun `starts on first tab at top level`() { val stack = newStack() assertEquals(HomeKey, stack.topLevelKey) assertEquals(HomeKey, stack.currentKey) assertTrue(stack.isTopLevel) assertFalse(stack.isOverride) assertEquals(listOf(HomeKey), stack.backStack.toList()) } @Test fun `switchTab changes selected tab and renders that tab root`() { val stack = newStack() stack.switchTab(SocialKey) assertEquals(SocialKey, stack.topLevelKey) assertEquals(SocialKey, stack.currentKey) assertTrue(stack.isTopLevel) assertEquals(listOf(SocialKey), stack.backStack.toList()) } @Test fun `push detail onto a tab is no longer top level`() { val stack = newStack() stack.push(PersonDetailKey("npc1")) assertEquals(PersonDetailKey("npc1"), stack.currentKey) assertFalse(stack.isTopLevel) assertFalse(stack.isOverride) assertEquals(listOf(HomeKey, PersonDetailKey("npc1")), stack.backStack.toList()) } @Test fun `each tab retains its own history across switches`() { val stack = newStack() // Build history on Home. stack.push(PersonDetailKey("npc1")) // Switch to Social and build different history. stack.switchTab(SocialKey) stack.push(ChatKey("npc2")) assertEquals(listOf(SocialKey, ChatKey("npc2")), stack.backStack.toList()) // Back to Home: its detail history is preserved. stack.switchTab(HomeKey) assertEquals(listOf(HomeKey, PersonDetailKey("npc1")), stack.backStack.toList()) // And Social still has its own stack. stack.switchTab(SocialKey) assertEquals(listOf(SocialKey, ChatKey("npc2")), stack.backStack.toList()) } @Test fun `pushing a top level destination switches tab instead of stacking`() { val stack = newStack() stack.push(StoreKey) assertEquals(StoreKey, stack.topLevelKey) assertEquals(listOf(StoreKey), stack.backStack.toList()) assertTrue(stack.isTopLevel) } @Test fun `removeLast pops within a tab then reports exit on first tab root`() { val stack = newStack() stack.push(PersonDetailKey("npc1")) stack.push(ChatKey("npc1")) assertTrue(stack.removeLast()) // pop ChatKey assertEquals(PersonDetailKey("npc1"), stack.currentKey) assertTrue(stack.removeLast()) // pop PersonDetailKey assertEquals(HomeKey, stack.currentKey) assertTrue(stack.isTopLevel) // At first tab root with no history => app should exit. assertFalse(stack.removeLast()) } @Test fun `removeLast on a non-first tab root falls back to the first tab`() { val stack = newStack() stack.switchTab(ActivitiesKey) assertEquals(ActivitiesKey, stack.currentKey) // At Activities root: back falls back to Home rather than exiting. assertTrue(stack.removeLast()) assertEquals(HomeKey, stack.topLevelKey) assertEquals(HomeKey, stack.currentKey) } @Test fun `resetTo an override key takes over the whole screen`() { val stack = newStack() stack.push(PersonDetailKey("npc1")) stack.switchTab(SocialKey) stack.push(ChatKey("npc2")) stack.resetTo(OnboardingKey) assertTrue(stack.isOverride) assertFalse(stack.isTopLevel) assertEquals(OnboardingKey, stack.currentKey) assertEquals(listOf(OnboardingKey), stack.backStack.toList()) } @Test fun `resetTo clears all tab history so tabs start fresh afterward`() { val stack = newStack() stack.push(PersonDetailKey("npc1")) stack.switchTab(SocialKey) stack.push(ChatKey("npc2")) stack.resetTo(DeathKey) // Leaving the override by switching to a tab shows a clean root. stack.switchTab(SocialKey) assertEquals(listOf(SocialKey), stack.backStack.toList()) stack.switchTab(HomeKey) assertEquals(listOf(HomeKey), stack.backStack.toList()) } @Test fun `resetTo a top level key clears override and selects that tab`() { val stack = newStack() stack.resetTo(OnboardingKey) assertTrue(stack.isOverride) stack.resetTo(StoreKey) assertFalse(stack.isOverride) assertTrue(stack.isTopLevel) assertEquals(StoreKey, stack.topLevelKey) assertEquals(listOf(StoreKey), stack.backStack.toList()) } @Test fun `switchTab exits an active override`() { val stack = newStack() stack.resetTo(OnboardingKey) assertTrue(stack.isOverride) stack.switchTab(ActivitiesKey) assertFalse(stack.isOverride) assertTrue(stack.isTopLevel) assertEquals(ActivitiesKey, stack.currentKey) } @Test fun `push while override is active stacks onto the override`() { val stack = newStack() stack.resetTo(OnboardingKey) stack.push(AchievementsKey) assertTrue(stack.isOverride) assertEquals(AchievementsKey, stack.currentKey) assertEquals(listOf(OnboardingKey, AchievementsKey), stack.backStack.toList()) // Popping the extra override entry returns to the base override. assertTrue(stack.removeLast()) assertEquals(OnboardingKey, stack.currentKey) assertTrue(stack.isOverride) } @Test fun `removeLast on a single override entry exits the override and falls back to tabs`() { val stack = newStack() stack.resetTo(DeathKey) assertTrue(stack.isOverride) // Popping the lone override entry clears the override; the flattened // stack then falls back to the (non-empty) tab root, so it returns true. assertTrue(stack.removeLast()) assertFalse(stack.isOverride) assertEquals(HomeKey, stack.currentKey) assertTrue(stack.isTopLevel) } @Test fun `custom start tab is respected`() { val stack = TabbedBackStack(TOP_LEVEL_DESTINATIONS, startTab = StoreKey) assertEquals(StoreKey, stack.topLevelKey) assertEquals(StoreKey, stack.currentKey) assertEquals(listOf(StoreKey), stack.backStack.toList()) } }