1
0
Fork 0

Add tests for collection creation controller

master
Tiger Oakes 2020-06-11 09:50:45 -07:00 committed by Emily Kager
parent 177e7a400f
commit 0b781ae3b7
3 changed files with 203 additions and 91 deletions

View File

@ -8,14 +8,14 @@ package org.mozilla.fenix.collections
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import org.mozilla.fenix.components.Analytics
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.home.Tab import org.mozilla.fenix.home.Tab
interface CollectionCreationController { interface CollectionCreationController {
@ -65,10 +65,10 @@ fun List<Tab>.toSessionBundle(sessionManager: SessionManager): List<Session> {
class DefaultCollectionCreationController( class DefaultCollectionCreationController(
private val store: CollectionCreationStore, private val store: CollectionCreationStore,
private val dismiss: () -> Unit, private val dismiss: () -> Unit,
private val analytics: Analytics, private val metrics: MetricController,
private val tabCollectionStorage: TabCollectionStorage, private val tabCollectionStorage: TabCollectionStorage,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val viewLifecycleScope: CoroutineScope private val scope: CoroutineScope
) : CollectionCreationController { ) : CollectionCreationController {
companion object { companion object {
@ -79,26 +79,31 @@ class DefaultCollectionCreationController(
override fun saveCollectionName(tabs: List<Tab>, name: String) { override fun saveCollectionName(tabs: List<Tab>, name: String) {
dismiss() dismiss()
val sessionBundle = tabs.toList().toSessionBundle(sessionManager) val sessionBundle = tabs.toSessionBundle(sessionManager)
viewLifecycleScope.launch(Dispatchers.IO) { scope.launch(IO) {
tabCollectionStorage.createCollection(name, sessionBundle) tabCollectionStorage.createCollection(name, sessionBundle)
} }
analytics.metrics.track( metrics.track(
Event.CollectionSaved(normalSessionSize(sessionManager), sessionBundle.size) Event.CollectionSaved(normalSessionSize(sessionManager), sessionBundle.size)
) )
} }
override fun renameCollection(collection: TabCollection, name: String) { override fun renameCollection(collection: TabCollection, name: String) {
dismiss() dismiss()
viewLifecycleScope.launch(Dispatchers.IO) { scope.launch(IO) {
tabCollectionStorage.renameCollection(collection, name) tabCollectionStorage.renameCollection(collection, name)
analytics.metrics.track(Event.CollectionRenamed)
} }
metrics.track(Event.CollectionRenamed)
} }
override fun backPressed(fromStep: SaveCollectionStep) { override fun backPressed(fromStep: SaveCollectionStep) {
handleBackPress(fromStep) val newStep = stepBack(fromStep)
if (newStep != null) {
store.dispatch(CollectionCreationAction.StepChanged(newStep))
} else {
dismiss()
}
} }
override fun selectAllTabs() { override fun selectAllTabs() {
@ -116,12 +121,12 @@ class DefaultCollectionCreationController(
override fun selectCollection(collection: TabCollection, tabs: List<Tab>) { override fun selectCollection(collection: TabCollection, tabs: List<Tab>) {
dismiss() dismiss()
val sessionBundle = tabs.toList().toSessionBundle(sessionManager) val sessionBundle = tabs.toList().toSessionBundle(sessionManager)
viewLifecycleScope.launch(Dispatchers.IO) { scope.launch(IO) {
tabCollectionStorage tabCollectionStorage
.addTabsToCollection(collection, sessionBundle) .addTabsToCollection(collection, sessionBundle)
} }
analytics.metrics.track( metrics.track(
Event.CollectionTabsAdded(normalSessionSize(sessionManager), sessionBundle.size) Event.CollectionTabsAdded(normalSessionSize(sessionManager), sessionBundle.size)
) )
} }
@ -171,25 +176,15 @@ class DefaultCollectionCreationController(
store.dispatch(CollectionCreationAction.TabRemoved(tab)) store.dispatch(CollectionCreationAction.TabRemoved(tab))
} }
private fun handleBackPress(backFromStep: SaveCollectionStep) { /**
val newStep = stepBack(backFromStep) * Will return the next valid state according to this diagram.
if (newStep != null) { *
store.dispatch(CollectionCreationAction.StepChanged(newStep)) * Name Collection -> Select Collection -> Select Tabs -> (dismiss fragment) <- Rename Collection
} else { */
dismiss()
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun stepBack( fun stepBack(
backFromStep: SaveCollectionStep backFromStep: SaveCollectionStep
): SaveCollectionStep? { ): SaveCollectionStep? {
/*
Will return the next valid state according to this diagram.
Name Collection -> Select Collection -> Select Tabs -> (dismiss fragment) <- Rename Collection
*/
val tabCollectionCount = store.state.tabCollections.size val tabCollectionCount = store.state.tabCollections.size
val tabCount = store.state.tabs.size val tabCount = store.state.tabs.size

View File

@ -74,10 +74,10 @@ class CollectionCreationFragment : DialogFragment() {
DefaultCollectionCreationController( DefaultCollectionCreationController(
collectionCreationStore, collectionCreationStore,
::dismiss, ::dismiss,
requireComponents.analytics, requireComponents.analytics.metrics,
requireComponents.core.tabCollectionStorage, requireComponents.core.tabCollectionStorage,
requireComponents.core.sessionManager, requireComponents.core.sessionManager,
viewLifecycleOwner.lifecycleScope scope = lifecycleScope
) )
) )
collectionCreationView = CollectionCreationView( collectionCreationView = CollectionCreationView(

View File

@ -5,48 +5,167 @@ import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import io.mockk.verifyAll
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.MediaState
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.components.Analytics
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.home.Tab
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class DefaultCollectionCreationControllerTest { class DefaultCollectionCreationControllerTest {
private val testCoroutineScope = TestCoroutineScope() private val testCoroutineScope = TestCoroutineScope()
private lateinit var state: CollectionCreationState
private lateinit var controller: DefaultCollectionCreationController private lateinit var controller: DefaultCollectionCreationController
@MockK(relaxed = true) private lateinit var store: CollectionCreationStore @MockK(relaxed = true) private lateinit var store: CollectionCreationStore
@MockK(relaxed = true) private lateinit var dismiss: () -> Unit @MockK(relaxed = true) private lateinit var dismiss: () -> Unit
@MockK(relaxed = true) private lateinit var analytics: Analytics @MockK(relaxUnitFun = true) private lateinit var metrics: MetricController
@MockK private lateinit var tabCollectionStorage: TabCollectionStorage @MockK(relaxUnitFun = true) private lateinit var tabCollectionStorage: TabCollectionStorage
@MockK private lateinit var tabsUseCases: TabsUseCases
@MockK private lateinit var sessionManager: SessionManager @MockK private lateinit var sessionManager: SessionManager
@MockK private lateinit var state: CollectionCreationState
@Before @Before
fun before() { fun before() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
every { store.state } returns state state = CollectionCreationState(
every { state.tabCollections } returns emptyList() tabCollections = emptyList(),
every { state.tabs } returns emptyList() tabs = emptyList()
)
every { store.state } answers { state }
controller = DefaultCollectionCreationController( controller = DefaultCollectionCreationController(
store, dismiss, analytics, store, dismiss, metrics,
tabCollectionStorage, sessionManager, testCoroutineScope tabCollectionStorage, sessionManager, testCoroutineScope
) )
} }
@Test
fun `GIVEN tab list WHEN saveCollectionName is called THEN collection should be created`() {
val session = mockSession(sessionId = "session-1")
val sessions = listOf(
session,
mockSession(sessionId = "session-2")
)
every { sessionManager.findSessionById("session-1") } returns session
every { sessionManager.findSessionById("null-session") } returns null
every { sessionManager.sessions } returns sessions
val tabs = listOf(
Tab("session-1", "", "", "", mediaState = MediaState.State.NONE),
Tab("null-session", "", "", "", mediaState = MediaState.State.NONE)
)
controller.saveCollectionName(tabs, "name")
verify { dismiss() }
verify { tabCollectionStorage.createCollection("name", listOf(session)) }
verify { metrics.track(Event.CollectionSaved(2, 1)) }
}
@Test
fun `GIVEN name collection WHEN backPressed is called THEN next step should be dispatched`() {
state = state.copy(tabCollections = listOf(mockk()))
controller.backPressed(SaveCollectionStep.NameCollection)
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.SelectCollection)) }
state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk(), mockk()))
controller.backPressed(SaveCollectionStep.NameCollection)
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.SelectTabs)) }
state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk()))
controller.backPressed(SaveCollectionStep.NameCollection)
verify { dismiss() }
}
@Test
fun `GIVEN select collection WHEN backPressed is called THEN next step should be dispatched`() {
state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk(), mockk()))
controller.backPressed(SaveCollectionStep.SelectCollection)
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.SelectTabs)) }
state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk()))
controller.backPressed(SaveCollectionStep.SelectCollection)
verify { dismiss() }
}
@Test
fun `GIVEN last step WHEN backPressed is called THEN dismiss should be called`() {
controller.backPressed(SaveCollectionStep.SelectTabs)
verify { dismiss() }
controller.backPressed(SaveCollectionStep.RenameCollection)
verify { dismiss() }
}
@Test
fun `GIVEN collection WHEN renameCollection is called THEN collection should be renamed`() = testCoroutineScope.runBlockingTest {
val collection = mockk<TabCollection>()
controller.renameCollection(collection, "name")
advanceUntilIdle()
verifyAll {
dismiss()
tabCollectionStorage.renameCollection(collection, "name")
metrics.track(Event.CollectionRenamed)
}
}
@Test
fun `WHEN select all is called THEN add all should be dispatched`() {
controller.selectAllTabs()
verify { store.dispatch(CollectionCreationAction.AddAllTabs) }
controller.deselectAllTabs()
verify { store.dispatch(CollectionCreationAction.RemoveAllTabs) }
controller.close()
verify { dismiss() }
}
@Test
fun `WHEN select tab is called THEN add tab should be dispatched`() {
val tab = mockk<Tab>()
controller.addTabToSelection(tab)
verify { store.dispatch(CollectionCreationAction.TabAdded(tab)) }
controller.removeTabFromSelection(tab)
verify { store.dispatch(CollectionCreationAction.TabRemoved(tab)) }
}
@Test
fun `WHEN selectCollection is called THEN add tabs should be added to collection`() {
val session = mockSession(sessionId = "session-1")
val sessions = listOf(
session,
mockSession(sessionId = "session-2")
)
every { sessionManager.findSessionById("session-1") } returns session
every { sessionManager.sessions } returns sessions
val tabs = listOf(
Tab("session-1", "", "", "", mediaState = MediaState.State.NONE)
)
val collection = mockk<TabCollection>()
controller.selectCollection(collection, tabs)
verify { dismiss() }
verify { tabCollectionStorage.addTabsToCollection(collection, listOf(session)) }
verify { metrics.track(Event.CollectionTabsAdded(2, 1)) }
}
@Test @Test
fun `GIVEN previous step was SelectTabs or RenameCollection WHEN stepBack is called THEN null should be returned`() { fun `GIVEN previous step was SelectTabs or RenameCollection WHEN stepBack is called THEN null should be returned`() {
assertNull(controller.stepBack(SaveCollectionStep.SelectTabs)) assertNull(controller.stepBack(SaveCollectionStep.SelectTabs))
@ -55,83 +174,79 @@ class DefaultCollectionCreationControllerTest {
@Test @Test
fun `GIVEN previous step was SelectCollection AND more than one tab is open WHEN stepBack is called THEN SelectTabs should be returned`() { fun `GIVEN previous step was SelectCollection AND more than one tab is open WHEN stepBack is called THEN SelectTabs should be returned`() {
every { state.tabs } returns listOf(mockk(), mockk()) state = state.copy(tabs = listOf(mockk(), mockk()))
assertEquals(SaveCollectionStep.SelectTabs, controller.stepBack(SaveCollectionStep.SelectCollection)) assertEquals(SaveCollectionStep.SelectTabs, controller.stepBack(SaveCollectionStep.SelectCollection))
} }
@Test @Test
fun `GIVEN previous step was SelectCollection AND one or fewer tabs are open WHEN stepbback is called THEN null should be returned`() { fun `GIVEN previous step was SelectCollection AND one or fewer tabs are open WHEN stepbback is called THEN null should be returned`() {
every { state.tabs } returns listOf(mockk()) state = state.copy(tabs = listOf(mockk()))
assertNull(controller.stepBack(SaveCollectionStep.SelectCollection)) assertNull(controller.stepBack(SaveCollectionStep.SelectCollection))
every { state.tabs } returns emptyList() state = state.copy(tabs = emptyList())
assertNull(controller.stepBack(SaveCollectionStep.SelectCollection)) assertNull(controller.stepBack(SaveCollectionStep.SelectCollection))
} }
@Test @Test
fun `GIVEN previous step was NameCollection AND tabCollections is empty AND more than one tab is open WHEN stepBack is called THEN SelectTabs should be returned`() { fun `GIVEN previous step was NameCollection AND tabCollections is empty AND more than one tab is open WHEN stepBack is called THEN SelectTabs should be returned`() {
every { state.tabCollections } returns emptyList() state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk(), mockk()))
every { state.tabs } returns listOf(mockk(), mockk())
assertEquals(SaveCollectionStep.SelectTabs, controller.stepBack(SaveCollectionStep.NameCollection)) assertEquals(SaveCollectionStep.SelectTabs, controller.stepBack(SaveCollectionStep.NameCollection))
} }
@Test @Test
fun `GIVEN previous step was NameCollection AND tabCollections is empty AND one or fewer tabs are open WHEN stepBack is called THEN null should be returned`() { fun `GIVEN previous step was NameCollection AND tabCollections is empty AND one or fewer tabs are open WHEN stepBack is called THEN null should be returned`() {
every { state.tabCollections } returns emptyList() state = state.copy(tabCollections = emptyList(), tabs = listOf(mockk()))
every { state.tabs } returns listOf(mockk())
assertNull(controller.stepBack(SaveCollectionStep.NameCollection)) assertNull(controller.stepBack(SaveCollectionStep.NameCollection))
every { state.tabCollections } returns emptyList() state = state.copy(tabCollections = emptyList(), tabs = emptyList())
every { state.tabs } returns emptyList()
assertNull(controller.stepBack(SaveCollectionStep.NameCollection)) assertNull(controller.stepBack(SaveCollectionStep.NameCollection))
} }
@Test @Test
fun `GIVEN previous step was NameCollection AND tabCollections is not empty WHEN stepBack is called THEN SelectCollection should be returned`() { fun `GIVEN previous step was NameCollection AND tabCollections is not empty WHEN stepBack is called THEN SelectCollection should be returned`() {
every { state.tabCollections } returns listOf(mockk()) state = state.copy(tabCollections = listOf(mockk()))
assertEquals(SaveCollectionStep.SelectCollection, controller.stepBack(SaveCollectionStep.NameCollection)) assertEquals(SaveCollectionStep.SelectCollection, controller.stepBack(SaveCollectionStep.NameCollection))
} }
@Test @Test
fun `GIVEN list of collections WHEN default collection number is required THEN return next default number`() { fun `GIVEN list of collections WHEN default collection number is required THEN return next default number`() {
val collections: MutableList<TabCollection> = ArrayList() val collections = mutableListOf<TabCollection>(
collections.add(mockk { mockk {
every { title } returns "Collection 1" every { title } returns "Collection 1"
}) },
collections.add(mockk { mockk {
every { title } returns "Collection 2" every { title } returns "Collection 2"
}) },
collections.add(mockk { mockk {
every { title } returns "Collection 3" every { title } returns "Collection 3"
}) }
every { state.tabCollections } returns collections )
state = state.copy(tabCollections = collections)
assertEquals(4, controller.getDefaultCollectionNumber()) assertEquals(4, controller.getDefaultCollectionNumber())
collections.add(mockk { collections.add(mockk {
every { title } returns "Collection 5" every { title } returns "Collection 5"
}) })
state = state.copy(tabCollections = collections)
assertEquals(6, controller.getDefaultCollectionNumber()) assertEquals(6, controller.getDefaultCollectionNumber())
collections.add(mockk { collections.add(mockk {
every { title } returns "Random name" every { title } returns "Random name"
}) })
state = state.copy(tabCollections = collections)
assertEquals(6, controller.getDefaultCollectionNumber()) assertEquals(6, controller.getDefaultCollectionNumber())
collections.add(mockk { collections.add(mockk {
every { title } returns "Collection 10 10" every { title } returns "Collection 10 10"
}) })
state = state.copy(tabCollections = collections)
assertEquals(6, controller.getDefaultCollectionNumber()) assertEquals(6, controller.getDefaultCollectionNumber())
} }
@Test @Test
fun `WHEN adding a new collection THEN dispatch NameCollection step changed`() { fun `WHEN adding a new collection THEN dispatch NameCollection step changed`() {
val collections: List<TabCollection> = ArrayList()
every { state.tabCollections } returns collections
controller.addNewCollection() controller.addNewCollection()
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.NameCollection, 1)) } verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.NameCollection, 1)) }
@ -139,9 +254,6 @@ class DefaultCollectionCreationControllerTest {
@Test @Test
fun `GIVEN empty list of collections WHEN saving tabs to collection THEN dispatch NameCollection step changed`() { fun `GIVEN empty list of collections WHEN saving tabs to collection THEN dispatch NameCollection step changed`() {
val collections: List<TabCollection> = ArrayList()
every { state.tabCollections } returns collections
controller.saveTabsToCollection(ArrayList()) controller.saveTabsToCollection(ArrayList())
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.NameCollection, 1)) } verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.NameCollection, 1)) }
@ -149,14 +261,14 @@ class DefaultCollectionCreationControllerTest {
@Test @Test
fun `GIVEN list of collections WHEN saving tabs to collection THEN dispatch NameCollection step changed`() { fun `GIVEN list of collections WHEN saving tabs to collection THEN dispatch NameCollection step changed`() {
val collections: MutableList<TabCollection> = ArrayList() state = state.copy(tabCollections = listOf(
collections.add(mockk { mockk {
every { title } returns "Collection 1" every { title } returns "Collection 1"
}) },
collections.add(mockk { mockk {
every { title } returns "Random Collection" every { title } returns "Random Collection"
}) }
every { state.tabCollections } returns collections ))
controller.saveTabsToCollection(ArrayList()) controller.saveTabsToCollection(ArrayList())
@ -165,27 +277,32 @@ class DefaultCollectionCreationControllerTest {
@Test @Test
fun `normalSessionSize only counts non-private non-custom sessions`() { fun `normalSessionSize only counts non-private non-custom sessions`() {
fun session(isPrivate: Boolean, isCustom: Boolean) = mockk<Session>().apply { val normal1 = mockSession()
every { private } returns isPrivate val normal2 = mockSession()
every { isCustomTabSession() } returns isCustom val normal3 = mockSession()
}
val normal1 = session(isPrivate = false, isCustom = false) val private1 = mockSession(isPrivate = true)
val normal2 = session(isPrivate = false, isCustom = false) val private2 = mockSession(isPrivate = true)
val normal3 = session(isPrivate = false, isCustom = false)
val private1 = session(isPrivate = true, isCustom = false) val custom1 = mockSession(isCustom = true)
val private2 = session(isPrivate = true, isCustom = false) val custom2 = mockSession(isCustom = true)
val custom3 = mockSession(isCustom = true)
val custom1 = session(isPrivate = false, isCustom = true) val privateCustom = mockSession(isPrivate = true, isCustom = true)
val custom2 = session(isPrivate = false, isCustom = true)
val custom3 = session(isPrivate = false, isCustom = true)
val privateCustom = session(isPrivate = true, isCustom = true)
every { sessionManager.sessions } returns listOf(normal1, private1, private2, custom1, every { sessionManager.sessions } returns listOf(normal1, private1, private2, custom1,
normal2, normal3, custom2, custom3, privateCustom) normal2, normal3, custom2, custom3, privateCustom)
assertEquals(3, controller.normalSessionSize(sessionManager)) assertEquals(3, controller.normalSessionSize(sessionManager))
} }
private fun mockSession(
sessionId: String? = null,
isPrivate: Boolean = false,
isCustom: Boolean = false
) = mockk<Session> {
sessionId?.let { every { id } returns it }
every { private } returns isPrivate
every { isCustomTabSession() } returns isCustom
}
} }