From 92a99542e635e3bcfdf3d7d957d066d00a623d8e Mon Sep 17 00:00:00 2001 From: Jonathan Almeida Date: Mon, 5 Apr 2021 23:12:07 +0400 Subject: [PATCH] Close #17821: Add TabsTrayStore with actions and reducer (#18773) --- .../mozilla/fenix/tabstray/TabsTrayStore.kt | 154 ++++++++++++++++++ .../fenix/tabstray/TrayPagerAdapter.kt | 6 +- .../fenix/tabstray/TabsTrayStoreTest.kt | 131 +++++++++++++++ .../org/mozilla/fenix/tabstray/browser/Tab.kt | 18 ++ .../browser/TabAdapterIdStorageTest.kt | 7 - 5 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt create mode 100644 app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/tabstray/browser/Tab.kt diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt new file mode 100644 index 000000000..bb8364666 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt @@ -0,0 +1,154 @@ +/* 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 + +import mozilla.components.concept.tabstray.Tab +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store + +/** + * Value type that represents the state of the tabs tray. + * + * @property selectedPage The current page in the tray can be on. + * @property mode Whether the browser tab list is in multi-select mode or not with the set of + * currently selected tabs. + * @property syncing Whether the Synced Tabs feature should fetch the latest tabs from paired + * devices. + */ +data class TabsTrayState( + val selectedPage: Page = Page.NormalTabs, + val mode: Mode = Mode.Normal, + val syncing: Boolean = false +) : State { + + /** + * The current mode that the tabs list is in. + */ + sealed class Mode { + + /** + * A set of selected tabs which we would want to perform an action on. + */ + open val selectedTabs = emptySet() + + /** + * The default mode the tabs list is in. + */ + object Normal : Mode() + + /** + * The multi-select mode that the tabs list is in containing the set of currently + * selected tabs. + */ + data class Select(override val selectedTabs: Set) : Mode() + } +} + +/** + * The different pagers in the tray that we can switch between in the [TrayPagerAdapter]. + */ +enum class Page { + + /** + * The pager position that displays normal tabs. + */ + NormalTabs, + + /** + * The pager position that displays private tabs. + */ + PrivateTabs, + + /** + * The pager position that displays Synced Tabs. + */ + SyncedTabs +} + +/** + * [Action] implementation related to [TabsTrayStore]. + */ +sealed class TabsTrayAction : Action { + + /** + * Entered multi-select mode. + */ + object EnterSelectMode : TabsTrayAction() + + /** + * Exited multi-select mode. + */ + object ExitSelectMode : TabsTrayAction() + + /** + * Added a new [Tab] to the selection set. + */ + data class AddSelectTab(val tab: Tab) : TabsTrayAction() + + /** + * Removed a [Tab] from the selection set. + */ + data class RemoveSelectTab(val tab: Tab) : TabsTrayAction() + + /** + * The active page in the tray that is now in focus. + */ + data class PageSelected(val page: Page) : TabsTrayAction() + + /** + * A request to perform a "sync" action. + */ + object SyncNow : TabsTrayAction() + + /** + * When a "sync" action has completed; this can be triggered immediately after [SyncNow] if + * no sync action was able to be performed. + */ + object SyncCompleted : TabsTrayAction() +} + +/** + * Reducer for [TabsTrayStore]. + */ +internal object TabsTrayReducer { + fun reduce(state: TabsTrayState, action: TabsTrayAction): TabsTrayState { + return when (action) { + is TabsTrayAction.EnterSelectMode -> + state.copy(mode = TabsTrayState.Mode.Select(emptySet())) + is TabsTrayAction.ExitSelectMode -> + state.copy(mode = TabsTrayState.Mode.Normal) + is TabsTrayAction.AddSelectTab -> + state.copy(mode = TabsTrayState.Mode.Select(state.mode.selectedTabs + action.tab)) + is TabsTrayAction.RemoveSelectTab -> { + val selected = state.mode.selectedTabs - action.tab + state.copy( + mode = if (selected.isEmpty()) { + TabsTrayState.Mode.Normal + } else { + TabsTrayState.Mode.Select(selected) + } + ) + } + is TabsTrayAction.PageSelected -> + state.copy(selectedPage = action.page) + is TabsTrayAction.SyncNow -> + state.copy(syncing = true) + is TabsTrayAction.SyncCompleted -> + state.copy(syncing = false) + } + } +} + +/** + * A [Store] that holds the [TabsTrayState] for the tabs tray and reduces [TabsTrayAction]s + * dispatched to the store. + */ +class TabsTrayStore( + initialState: TabsTrayState = TabsTrayState() +) : Store( + initialState, + TabsTrayReducer::reduce +) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt index 9d2ce0403..6fdf2b3e6 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt @@ -74,8 +74,8 @@ class TrayPagerAdapter( companion object { const val TRAY_TABS_COUNT = 3 - const val POSITION_NORMAL_TABS = 0 - const val POSITION_PRIVATE_TABS = 1 - const val POSITION_SYNCED_TABS = 2 + val POSITION_NORMAL_TABS = Page.NormalTabs.ordinal + val POSITION_PRIVATE_TABS = Page.PrivateTabs.ordinal + val POSITION_SYNCED_TABS = Page.SyncedTabs.ordinal } } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreTest.kt new file mode 100644 index 000000000..a84ff3344 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreTest.kt @@ -0,0 +1,131 @@ +/* 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 + +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mozilla.fenix.tabstray.browser.createTab + +class TabsTrayStoreTest { + + @Test + fun `WHEN entering select mode THEN selected tabs are empty`() { + val store = TabsTrayStore() + + store.dispatch(TabsTrayAction.EnterSelectMode) + + store.waitUntilIdle() + + assertTrue(store.state.mode.selectedTabs.isEmpty()) + assertTrue(store.state.mode is TabsTrayState.Mode.Select) + + store.dispatch(TabsTrayAction.AddSelectTab(createTab())) + + store.dispatch(TabsTrayAction.ExitSelectMode) + store.dispatch(TabsTrayAction.EnterSelectMode) + + store.waitUntilIdle() + + assertTrue(store.state.mode.selectedTabs.isEmpty()) + assertTrue(store.state.mode is TabsTrayState.Mode.Select) + } + + @Test + fun `WHEN exiting select mode THEN the mode in the state updates`() { + val store = TabsTrayStore() + + store.dispatch(TabsTrayAction.EnterSelectMode) + + store.waitUntilIdle() + + assertTrue(store.state.mode is TabsTrayState.Mode.Select) + + store.dispatch(TabsTrayAction.ExitSelectMode) + + store.waitUntilIdle() + + assertTrue(store.state.mode is TabsTrayState.Mode.Normal) + } + + @Test + fun `WHEN adding a tab to selection THEN it is added to the selectedTabs`() { + val store = TabsTrayStore() + + store.dispatch(TabsTrayAction.AddSelectTab(createTab("tab1"))) + + store.waitUntilIdle() + + assertEquals("tab1", store.state.mode.selectedTabs.take(1).first().id) + } + + @Test + fun `WHEN removing a tab THEN it is removed from the selectedTabs`() { + val store = TabsTrayStore() + val tabForRemoval = createTab("tab1") + + store.dispatch(TabsTrayAction.AddSelectTab(tabForRemoval)) + store.dispatch(TabsTrayAction.AddSelectTab(createTab("tab2"))) + + store.waitUntilIdle() + + assertEquals(2, store.state.mode.selectedTabs.size) + + store.dispatch(TabsTrayAction.RemoveSelectTab(tabForRemoval)) + + store.waitUntilIdle() + + assertEquals(1, store.state.mode.selectedTabs.size) + assertEquals("tab2", store.state.mode.selectedTabs.take(1).first().id) + } + + @Test + fun `WHEN store is initialized THEN the default page selected in normal tabs`() { + val store = TabsTrayStore() + + assertEquals(Page.NormalTabs, store.state.selectedPage) + } + + @Test + fun `WHEN page changes THEN the selectedPage is updated`() { + val store = TabsTrayStore() + + assertEquals(Page.NormalTabs, store.state.selectedPage) + + store.dispatch(TabsTrayAction.PageSelected(Page.SyncedTabs)) + + store.waitUntilIdle() + + assertEquals(Page.SyncedTabs, store.state.selectedPage) + } + + @Test + fun `WHEN sync now action is triggered THEN update the sync now boolean`() { + val store = TabsTrayStore() + + assertFalse(store.state.syncing) + + store.dispatch(TabsTrayAction.SyncNow) + + store.waitUntilIdle() + + assertTrue(store.state.syncing) + } + + @Test + fun `WHEN sync is complete THEN the syncing boolean is updated`() { + val store = TabsTrayStore(initialState = TabsTrayState(syncing = true)) + + assertTrue(store.state.syncing) + + store.dispatch(TabsTrayAction.SyncCompleted) + + store.waitUntilIdle() + + assertFalse(store.state.syncing) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/Tab.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/Tab.kt new file mode 100644 index 000000000..738ee7fca --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/Tab.kt @@ -0,0 +1,18 @@ +/* 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.browser + +import mozilla.components.concept.tabstray.Tab +import java.util.UUID + +/** + * Helper for writing tests that need a [Tab]. + */ +fun createTab( + tabId: String = UUID.randomUUID().toString() +) = Tab( + tabId, + "https://mozilla.org" +) diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabAdapterIdStorageTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabAdapterIdStorageTest.kt index 881112946..e67903edd 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabAdapterIdStorageTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabAdapterIdStorageTest.kt @@ -4,13 +4,11 @@ package org.mozilla.fenix.tabstray.browser -import mozilla.components.concept.tabstray.Tab import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import java.util.UUID @RunWith(FenixRobolectricTestRunner::class) class TabAdapterIdStorageTest { @@ -78,8 +76,3 @@ class TabAdapterIdStorageTest { assertNotEquals(id2, id3) } } - -fun createTab() = Tab( - UUID.randomUUID().toString(), - "https://mozilla.org" -)