tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 0e50f7e0a401f0b63d06e74ee5babfcaa86a38e1
parent 3041b115e90fe11124e645788b3b95cef64bf432
Author: Julie De Lorenzo <jdelorenzo@mozilla.com>
Date:   Mon, 15 Dec 2025 19:51:51 +0000

Bug 2003697:  Move synced tabs collapsed state to TabsTrayState r=android-reviewers,007

Differential Revision: https://phabricator.services.mozilla.com/D275695

Diffstat:
Amobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/tabstray/ui/SyncedTabListTest.kt | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsPage.kt | 7+++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt | 5++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsList.kt | 23++++++++++++++---------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/syncedtabs/SyncedTabsList.kt | 19++++++++++---------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabpage/SyncedTabsPage.kt | 8++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabstray/TabsTray.kt | 7++++++-
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreReducerTest.kt | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
9 files changed, 274 insertions(+), 23 deletions(-)

diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/tabstray/ui/SyncedTabListTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/tabstray/ui/SyncedTabListTest.kt @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.fenix.tabstray.ui + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onChildAt +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.browser.storage.sync.TabEntry +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.tabstray.TabsTrayTestTag.SYNCED_TABS_LIST +import org.mozilla.fenix.tabstray.syncedtabs.OnSectionExpansionToggled +import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem +import org.mozilla.fenix.tabstray.ui.syncedtabs.SyncedTabsList +import mozilla.components.browser.storage.sync.Tab as SyncTab + +@RunWith(AndroidJUnit4::class) +class SyncedTabListTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testClickingSyncedTabHeaderInvokesCallback() { + var expansionToggled = false + + val fakeTabs = generateFakeSyncedTabsList(3) + composeTestRule.setContent { + SyncedTabsList( + syncedTabs = fakeTabs, + onTabClick = { println("Tab clicked") }, + onTabCloseClick = { _, _ -> println("Tab closed") }, + onSectionExpansionToggled = { expansionToggled = true }, + expandedState = fakeTabs.map { true }, + ) + } + + composeTestRule.onNodeWithTag(SYNCED_TABS_LIST).onChildAt(0).performClick() + + assertTrue(expansionToggled) + } + + private fun generateFakeSyncedTabsList(deviceCount: Int = 1): List<SyncedTabsListItem> = + List(deviceCount) { index -> + SyncedTabsListItem.DeviceSection( + displayName = "Device $index", + tabs = listOf( + generateFakeSyncedTab("Mozilla", "www.mozilla.org"), + generateFakeSyncedTab("Google", "www.google.com"), + generateFakeSyncedTab("", "www.google.com"), + ), + ) + } + + private fun generateFakeSyncedTab( + tabName: String, + tabUrl: String, + action: SyncedTabsListItem.Tab.Action = SyncedTabsListItem.Tab.Action.None, + ): SyncedTabsListItem.Tab = + SyncedTabsListItem.Tab( + tabName.ifEmpty { tabUrl }, + tabUrl, + action, + SyncTab( + history = listOf(TabEntry(tabName, tabUrl, null)), + active = 0, + lastUsed = 0L, + inactive = false, + ), + ) +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsPage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsPage.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.tabstray import androidx.compose.runtime.Composable +import org.mozilla.fenix.tabstray.syncedtabs.OnSectionExpansionToggled import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsList import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem import org.mozilla.fenix.tabstray.syncedtabs.OnTabClick as OnSyncedTabClick @@ -16,16 +17,22 @@ import org.mozilla.fenix.tabstray.syncedtabs.OnTabCloseClick as OnSyncedTabClose * @param syncedTabs The list of [SyncedTabsListItem] to display. * @param onTabClick Invoked when the user clicks on a tab. * @param onTabClose Invoked when the user clicks to close a tab. + * @param expandedState The list of [SyncedTabsListItem] expansion states. + * @param onSectionExpansionToggled Invoked when a user toggles the section expansion. */ @Composable internal fun SyncedTabsPage( syncedTabs: List<SyncedTabsListItem>, onTabClick: OnSyncedTabClick, onTabClose: OnSyncedTabClose, + expandedState: List<Boolean>, + onSectionExpansionToggled: OnSectionExpansionToggled, ) { SyncedTabsList( syncedTabs = syncedTabs, onTabClick = onTabClick, onTabCloseClick = onTabClose, + expandedState = expandedState, + onSectionExpansionToggled = onSectionExpansionToggled, ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt @@ -220,7 +220,6 @@ fun TabsTray( HorizontalPager( modifier = Modifier.fillMaxSize(), state = pagerState, - beyondViewportPageCount = 2, userScrollEnabled = false, ) { position -> when (Page.positionToPage(position = position, enhancementsEnabled = false)) { @@ -276,6 +275,10 @@ fun TabsTray( syncedTabs = tabsTrayState.syncedTabs, onTabClick = onSyncedTabClick, onTabClose = onSyncedTabClose, + expandedState = tabsTrayState.expandedSyncedTabs, + onSectionExpansionToggled = { index -> + tabsTrayStore.dispatch(TabsTrayAction.SyncedTabsHeaderToggled(index)) + }, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt @@ -13,6 +13,8 @@ import mozilla.components.lib.state.Store import org.mozilla.fenix.tabstray.navigation.TabManagerNavDestination import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem +private const val DEFAULT_SYNCED_TABS_EXPANDED_STATE = true + /** * Value type that represents the state of the tabs tray. * @@ -28,6 +30,7 @@ import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem * @property syncing Whether the Synced Tabs feature should fetch the latest tabs from paired devices. * @property selectedTabId The ID of the currently selected (active) tab. * @property backStack The navigation history of the Tab Manager feature. + * @property expandedSyncedTabs The list of expansion states for the syncedTabs. */ data class TabsTrayState( val selectedPage: Page = Page.NormalTabs, @@ -40,6 +43,7 @@ data class TabsTrayState( val syncing: Boolean = false, val selectedTabId: String? = null, val backStack: List<TabManagerNavDestination> = listOf(TabManagerNavDestination.Root), + val expandedSyncedTabs: List<Boolean> = emptyList(), ) : State { /** @@ -228,6 +232,13 @@ sealed class TabsTrayAction : Action { data class UpdateSelectedTabId(val tabId: String?) : TabsTrayAction() /** + * Expands or collapses the header on the synced tabs page. + * + * @property index The index of the header. + */ + data class SyncedTabsHeaderToggled(val index: Int) : TabsTrayAction() + + /** * [TabsTrayAction] fired when the tab auto close dialog is shown. */ object TabAutoCloseDialogShown : TabsTrayAction() @@ -309,8 +320,7 @@ internal object TabsTrayReducer { state.copy(normalTabs = action.tabs) is TabsTrayAction.UpdatePrivateTabs -> state.copy(privateTabs = action.tabs) - is TabsTrayAction.UpdateSyncedTabs -> - state.copy(syncedTabs = action.tabs) + is TabsTrayAction.UpdateSyncedTabs -> handleSyncedTabUpdate(state, action) is TabsTrayAction.UpdateSelectedTabId -> state.copy(selectedTabId = action.tabId) is TabsTrayAction.TabAutoCloseDialogShown -> state @@ -328,11 +338,66 @@ internal object TabsTrayReducer { else -> state.copy(backStack = state.backStack.dropLast(1)) } } + is TabsTrayAction.SyncedTabsHeaderToggled -> handleSyncedTabHeaderToggle(state, action) } } } /** + * Updates the synced tabs list. Also updates the expansion state of the tabs. + * If items are identical in an existing list, their selection state will be preserved + * (pressing sync tab on an already synced tab will not reset your expansion selections). + * If the tab list is updated or no tabs existed previously, selections will be the default value. + * + * @param state the existing state object + * @param action the action containing updated tabs. + */ +private fun handleSyncedTabUpdate(state: TabsTrayState, action: TabsTrayAction.UpdateSyncedTabs): TabsTrayState { + return if (state.syncedTabs.isNotEmpty() && action.tabs.isNotEmpty()) { + state.copy( + syncedTabs = action.tabs, + expandedSyncedTabs = action.tabs.mapIndexed { index, item -> + if (state.syncedTabs[index] == item) { + state.expandedSyncedTabs[index] + } else { + DEFAULT_SYNCED_TABS_EXPANDED_STATE + } + }, + ) + } else if (action.tabs.isNotEmpty()) { + state.copy( + syncedTabs = action.tabs, + expandedSyncedTabs = + action.tabs.map { DEFAULT_SYNCED_TABS_EXPANDED_STATE }, + ) + } else { + state.copy(syncedTabs = action.tabs, expandedSyncedTabs = emptyList()) + } +} + +/** + * When a synced tab header's expansion is toggled, that item should be expanded or collapsed. + * The rest of the list should be unchanged. + * + * @param state the existing state object + * @param action the action containing the index of the toggled header. + */ +private fun handleSyncedTabHeaderToggle( + state: TabsTrayState, + action: TabsTrayAction.SyncedTabsHeaderToggled, +): TabsTrayState { + return state.copy( + expandedSyncedTabs = state.expandedSyncedTabs.mapIndexed { index, isExpanded -> + if (index == action.index) { + !isExpanded + } else { + isExpanded + } + }, + ) +} + +/** * A [Store] that holds the [TabsTrayState] for the tabs tray and reduces [TabsTrayAction]s * dispatched to the store. */ diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsList.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsList.kt @@ -25,8 +25,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -46,8 +44,6 @@ import org.mozilla.fenix.theme.FirefoxTheme import mozilla.components.browser.storage.sync.Tab as SyncTab import mozilla.components.ui.icons.R as iconsR -private const val EXPANDED_BY_DEFAULT = true - /** * A lambda invoked when the user clicks on a synced tab in the [SyncedTabsList]. */ @@ -59,11 +55,18 @@ typealias OnTabClick = (tab: SyncTab) -> Unit typealias OnTabCloseClick = (deviceId: String, tab: SyncTab) -> Unit /** + * A lambda invoked when the expands a section in the [SyncedTabsList]. + */ +typealias OnSectionExpansionToggled = (index: Int) -> Unit + +/** * Top-level list UI for displaying Synced Tabs in the Tabs Tray. * * @param syncedTabs The tab UI items to be displayed. * @param onTabClick The lambda for handling clicks on synced tabs. * @param onTabCloseClick The lambda for handling clicks on a synced tab's close button. + * @param expandedState A list of expanded state properties for the synced tabs. + * @param onSectionExpansionToggled A lambda for handling section expansion/collapse. */ @SuppressWarnings("LongMethod", "CognitiveComplexMethod") @Composable @@ -71,11 +74,10 @@ fun SyncedTabsList( syncedTabs: List<SyncedTabsListItem>, onTabClick: OnTabClick, onTabCloseClick: OnTabCloseClick, + expandedState: List<Boolean>, + onSectionExpansionToggled: OnSectionExpansionToggled, ) { val listState = rememberLazyListState() - val expandedState = - remember(syncedTabs) { syncedTabs.map { EXPANDED_BY_DEFAULT }.toMutableStateList() } - LazyColumn( modifier = Modifier .fillMaxSize() @@ -92,7 +94,7 @@ fun SyncedTabsList( headerText = syncedTabItem.displayName, expanded = sectionExpanded, ) { - expandedState[index] = !sectionExpanded + onSectionExpansionToggled(index) } } @@ -295,12 +297,15 @@ private fun SyncedTabsErrorPreview() { @Composable @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) private fun SyncedTabsListPreview() { + val syncedTabList = getFakeSyncedTabList() FirefoxTheme { Surface { SyncedTabsList( - syncedTabs = getFakeSyncedTabList(), + syncedTabs = syncedTabList, onTabClick = { println("Tab clicked") }, onTabCloseClick = { _, _ -> println("Tab closed") }, + expandedState = syncedTabList.map { true }, + onSectionExpansionToggled = {}, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/syncedtabs/SyncedTabsList.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/syncedtabs/SyncedTabsList.kt @@ -30,8 +30,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -48,13 +46,12 @@ import mozilla.components.feature.syncedtabs.view.SyncedTabsView import org.mozilla.fenix.R import org.mozilla.fenix.compose.list.ExpandableListHeader import org.mozilla.fenix.tabstray.TabsTrayTestTag +import org.mozilla.fenix.tabstray.syncedtabs.OnSectionExpansionToggled import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem import org.mozilla.fenix.tabstray.ui.tabitems.BasicTabListItem import org.mozilla.fenix.theme.FirefoxTheme import mozilla.components.browser.storage.sync.Tab as SyncTab import mozilla.components.ui.icons.R as iconsR - -private const val EXPANDED_BY_DEFAULT = true private val CardRoundedCornerShape = RoundedCornerShape(12.dp) /** @@ -73,6 +70,8 @@ typealias OnTabCloseClick = (deviceId: String, tab: SyncTab) -> Unit * @param syncedTabs The tab UI items to be displayed. * @param onTabClick The lambda for handling clicks on synced tabs. * @param onTabCloseClick The lambda for handling clicks on a synced tab's close button. + * @param expandedState A list of expanded state properties for the synced tabs. + * @param onSectionExpansionToggled A lambda for handling section expansion/collapse */ @SuppressWarnings("LongMethod", "CognitiveComplexMethod") @Composable @@ -80,11 +79,10 @@ fun SyncedTabsList( syncedTabs: List<SyncedTabsListItem>, onTabClick: OnTabClick, onTabCloseClick: OnTabCloseClick, + expandedState: List<Boolean>, + onSectionExpansionToggled: OnSectionExpansionToggled, ) { val listState = rememberLazyListState() - val expandedState = - remember(syncedTabs) { syncedTabs.map { EXPANDED_BY_DEFAULT }.toMutableStateList() } - Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter, @@ -107,7 +105,7 @@ fun SyncedTabsList( headerText = syncedTabItem.displayName, expanded = sectionExpanded, ) { - expandedState[index] = !sectionExpanded + onSectionExpansionToggled.invoke(index) } } @@ -341,12 +339,15 @@ private fun SyncedTabsErrorPreview() { @Composable @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) private fun SyncedTabsListPreview() { + val syncedTabsList = getFakeSyncedTabList() FirefoxTheme { Surface { SyncedTabsList( - syncedTabs = getFakeSyncedTabList(), + syncedTabs = syncedTabsList, onTabClick = { println("Tab clicked") }, onTabCloseClick = { _, _ -> println("Tab closed") }, + onSectionExpansionToggled = {}, + expandedState = syncedTabsList.map { true }, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabpage/SyncedTabsPage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabpage/SyncedTabsPage.kt @@ -26,9 +26,11 @@ import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview import mozilla.components.compose.base.button.TextButton import org.mozilla.fenix.R import org.mozilla.fenix.tabstray.TabsTrayTestTag +import org.mozilla.fenix.tabstray.syncedtabs.OnSectionExpansionToggled import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem import org.mozilla.fenix.tabstray.ui.syncedtabs.SyncedTabsList import org.mozilla.fenix.theme.FirefoxTheme +import kotlin.Boolean import mozilla.components.ui.icons.R as iconsR import org.mozilla.fenix.tabstray.ui.syncedtabs.OnTabClick as OnSyncedTabClick import org.mozilla.fenix.tabstray.ui.syncedtabs.OnTabCloseClick as OnSyncedTabClose @@ -43,6 +45,8 @@ private val EmptyPageWidth = 200.dp * @param onTabClick Invoked when the user clicks on a tab. * @param onTabClose Invoked when the user clicks to close a tab. * @param onSignInClick Invoked when an unauthenticated user clicks to sign-in. + * @param expandedState The list of [SyncedTabsListItem] expansion states. + * @param onSectionExpansionToggled Invoked when a user toggles the section expansion. */ @Composable internal fun SyncedTabsPage( @@ -51,12 +55,16 @@ internal fun SyncedTabsPage( onTabClick: OnSyncedTabClick, onTabClose: OnSyncedTabClose, onSignInClick: () -> Unit, + expandedState: List<Boolean>, + onSectionExpansionToggled: OnSectionExpansionToggled, ) { if (isSignedIn) { SyncedTabsList( syncedTabs = syncedTabs, onTabClick = onTabClick, onTabCloseClick = onTabClose, + expandedState = expandedState, + onSectionExpansionToggled = onSectionExpansionToggled, ) } else { UnauthenticatedSyncedTabsPage(onSignInClick = onSignInClick) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabstray/TabsTray.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabstray/TabsTray.kt @@ -269,7 +269,6 @@ fun TabsTray( .padding(paddingValues) .fillMaxSize(), state = pagerState, - beyondViewportPageCount = 2, userScrollEnabled = false, ) { position -> when (Page.positionToPage(position)) { @@ -325,6 +324,10 @@ fun TabsTray( onTabClick = onSyncedTabClick, onTabClose = onSyncedTabClose, onSignInClick = onSignInClick, + expandedState = tabsTrayState.expandedSyncedTabs, + onSectionExpansionToggled = { i -> + tabsTrayStore.dispatch(TabsTrayAction.SyncedTabsHeaderToggled(i)) + }, ) } } @@ -368,6 +371,7 @@ private fun TabsTrayPreview( privateTabs = tabTrayState.privateTabs, syncedTabs = tabTrayState.syncedTabs, selectedTabId = tabTrayState.selectedTabId, + expandedSyncedTabs = tabTrayState.expandedSyncedTabs, ), ) } @@ -581,6 +585,7 @@ private data class TabsTrayPreviewModel( val showTabAutoCloseBanner: Boolean = false, val isPbmLocked: Boolean = false, val isSignedIn: Boolean = true, + val expandedSyncedTabs: List<Boolean> = emptyList(), ) private fun generateFakeTabsList( diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreReducerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreReducerTest.kt @@ -82,7 +82,7 @@ class TabsTrayStoreReducerTest { fun `WHEN UpdateSyncedTabs THEN synced tabs are added`() { val syncedTabs = getFakeSyncedTabList() val initialState = TabsTrayState() - val expectedState = initialState.copy(syncedTabs = syncedTabs) + val expectedState = initialState.copy(syncedTabs = syncedTabs, expandedSyncedTabs = syncedTabs.map { true }) val resultState = TabsTrayReducer.reduce( initialState, @@ -93,6 +93,60 @@ class TabsTrayStoreReducerTest { } @Test + fun `GIVEN no synced tabs WHEN UpdateSyncedTabs is called with tabs THEN the expanded state is initialized to true`() { + val initialState = TabsTrayState() + val syncedTabs = getFakeSyncedTabList() + + val resultState = TabsTrayReducer.reduce( + initialState, + TabsTrayAction.UpdateSyncedTabs(syncedTabs), + ) + + assertTrue(resultState.expandedSyncedTabs.all { true }) + } + + @Test + fun `WHEN UpdateSyncedTabs is called with an empty list THEN the expanded state is set to an empty list`() { + val initialState = TabsTrayState() + + val resultState = TabsTrayReducer.reduce( + initialState, + TabsTrayAction.UpdateSyncedTabs(emptyList()), + ) + + assertTrue(resultState.expandedSyncedTabs.isEmpty()) + } + + @Test + fun `GIVEN synced tabs WHEN UpdateSyncedTabs is called with the same tabs THEN the expanded state is retained`() { + val expectedExpansionList = listOf(true, true, false, false) + val syncedTabs = getFakeSyncedTabList() + val initialState = TabsTrayState(syncedTabs = syncedTabs, expandedSyncedTabs = expectedExpansionList) + + val resultState = TabsTrayReducer.reduce( + initialState, + TabsTrayAction.UpdateSyncedTabs(syncedTabs), + ) + + assertTrue(resultState.expandedSyncedTabs == expectedExpansionList) + } + + @Test + fun `GIVEN synced tabs WHEN UpdateSyncedTabs is called with different tabs THEN the expanded state is reset`() { + val expectedExpansionList = listOf(true, true, false, false) + val syncedTabs = getFakeSyncedTabList() + val newSyncedTabs = syncedTabs.reversed() + val initialState = TabsTrayState(syncedTabs = syncedTabs, expandedSyncedTabs = expectedExpansionList) + + val resultState = TabsTrayReducer.reduce( + initialState, + TabsTrayAction.UpdateSyncedTabs(newSyncedTabs), + ) + + assertTrue(resultState.expandedSyncedTabs.all { true }) + } + + @Test fun `WHEN the tab search button is pressed THEN the tab search destination is added to the back stack`() { val initialState = TabsTrayState() val resultState = TabsTrayReducer.reduce( @@ -103,4 +157,30 @@ class TabsTrayStoreReducerTest { assertTrue(initialState.backStack.none { it == TabManagerNavDestination.TabSearch }) assertTrue(resultState.backStack.last() == TabManagerNavDestination.TabSearch) } + + @Test + fun `GIVEN the synced tab header is expanded WHEN the synced tabs header is toggled THEN the synced tabs header is collapsed`() { + val syncedTabs = getFakeSyncedTabList() + val initialState = TabsTrayState(syncedTabs = syncedTabs, expandedSyncedTabs = syncedTabs.map { true }) + + val resultState = TabsTrayReducer.reduce( + state = initialState, + action = TabsTrayAction.SyncedTabsHeaderToggled(0), + ) + + assertFalse(resultState.expandedSyncedTabs[0]) + } + + @Test + fun `GIVEN the synced tab header is collapsed WHEN the synced tabs header is toggled THEN the synced tabs header is expanded`() { + val syncedTabs = getFakeSyncedTabList() + val initialState = TabsTrayState(syncedTabs = syncedTabs, expandedSyncedTabs = syncedTabs.map { false }) + + val resultState = TabsTrayReducer.reduce( + state = initialState, + action = TabsTrayAction.SyncedTabsHeaderToggled(0), + ) + + assertTrue(resultState.expandedSyncedTabs[0]) + } }