1
0
Fork 0

Add tests for some of collection creation

master
Tiger Oakes 2020-05-29 10:56:14 -07:00 committed by Emily Kager
parent b5d03ff62d
commit 6317eea690
4 changed files with 346 additions and 142 deletions

View File

@ -0,0 +1,96 @@
/* 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.collections
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import mozilla.components.support.ktx.android.content.getDrawableWithTint
import org.mozilla.fenix.R
/**
* Updates views in [R.id.bottom_button_bar_layout] for collection creation.
*/
class CollectionCreationBottomBarView(
private val interactor: CollectionCreationInteractor,
private val layout: ViewGroup,
private val iconButton: ImageButton,
private val textView: TextView,
private val saveButton: Button
) {
fun update(step: SaveCollectionStep, state: CollectionCreationState) {
when (step) {
SaveCollectionStep.SelectTabs -> updateForSelectTabs(state)
SaveCollectionStep.SelectCollection -> updateForSelectCollection()
else -> { /* noop */ }
}
}
private fun updateForSelectTabs(state: CollectionCreationState) {
layout.setOnClickListener(null)
layout.isClickable = false
iconButton.apply {
val drawable = context.getDrawableWithTint(
R.drawable.ic_close,
ContextCompat.getColor(context, R.color.photonWhite)
)
setImageDrawable(drawable)
contentDescription = context.getString(R.string.create_collection_close)
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
setOnClickListener { interactor.close() }
}
textView.apply {
text = if (state.selectedTabs.isEmpty()) {
context.getString(R.string.create_collection_save_to_collection_empty)
} else {
context.getString(
if (state.selectedTabs.size == 1)
R.string.create_collection_save_to_collection_tab_selected else
R.string.create_collection_save_to_collection_tabs_selected,
state.selectedTabs.size
)
}
}
saveButton.apply {
setOnClickListener {
if (state.selectedTabCollection != null) {
interactor.selectCollection(
collection = state.selectedTabCollection,
tabs = state.selectedTabs.toList()
)
} else {
interactor.saveTabsToCollection(tabs = state.selectedTabs.toList())
}
}
isVisible = state.selectedTabs.isNotEmpty()
}
}
private fun updateForSelectCollection() {
saveButton.visibility = View.GONE
textView.text =
textView.context.getString(R.string.create_collection_add_new_collection)
iconButton.apply {
val drawable = context.getDrawableWithTint(
R.drawable.ic_new,
ContextCompat.getColor(context, R.color.photonWhite)
)
setImageDrawable(drawable)
contentDescription = null
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
setOnClickListener { interactor.addNewCollection() }
}
}
}

View File

@ -24,7 +24,7 @@ class CollectionCreationStore(
* *
* TODO refactor [CollectionCreationState] into a sealed class with four implementations, each * TODO refactor [CollectionCreationState] into a sealed class with four implementations, each
* replacing a [SaveCollectionStep] value. These will not need null / emptyCollection default * replacing a [SaveCollectionStep] value. These will not need null / emptyCollection default
* values. Handle changes bebtween these state changes internally, here and in the controller, * values. Handle changes between these state changes internally, here and in the controller,
* instead of exposing [StepChanged], which currently acts as a setter. * instead of exposing [StepChanged], which currently acts as a setter.
*/ */
enum class SaveCollectionStep { enum class SaveCollectionStep {

View File

@ -11,28 +11,14 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.transition.AutoTransition import androidx.transition.AutoTransition
import androidx.transition.Transition import androidx.transition.Transition
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_collection_creation.back_button import kotlinx.android.synthetic.main.component_collection_creation.*
import kotlinx.android.synthetic.main.component_collection_creation.collection_constraint_layout
import kotlinx.android.synthetic.main.component_collection_creation.name_collection_edittext
import kotlinx.android.synthetic.main.component_collection_creation.save_button
import kotlinx.android.synthetic.main.component_collection_creation.select_all_button
import kotlinx.android.synthetic.main.component_collection_creation.view.bottom_bar_icon_button
import kotlinx.android.synthetic.main.component_collection_creation.view.bottom_bar_text
import kotlinx.android.synthetic.main.component_collection_creation.view.bottom_button_bar_layout
import kotlinx.android.synthetic.main.component_collection_creation.view.collection_constraint_layout
import kotlinx.android.synthetic.main.component_collection_creation.view.collections_list
import kotlinx.android.synthetic.main.component_collection_creation.view.name_collection_edittext
import kotlinx.android.synthetic.main.component_collection_creation.view.select_all_button
import kotlinx.android.synthetic.main.component_collection_creation.view.tab_list
import mozilla.components.browser.state.state.MediaState import mozilla.components.browser.state.state.MediaState
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
@ -43,14 +29,21 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.home.Tab import org.mozilla.fenix.home.Tab
@SuppressWarnings("LargeClass")
class CollectionCreationView( class CollectionCreationView(
override val containerView: ViewGroup, container: ViewGroup,
private val interactor: CollectionCreationInteractor private val interactor: CollectionCreationInteractor
) : LayoutContainer { ) : LayoutContainer {
val view: View = LayoutInflater.from(containerView.context)
.inflate(R.layout.component_collection_creation, containerView, true)
override val containerView: View = LayoutInflater.from(container.context)
.inflate(R.layout.component_collection_creation, container, true)
private val bottomBarView = CollectionCreationBottomBarView(
interactor = interactor,
layout = bottom_button_bar_layout,
iconButton = bottom_bar_icon_button,
textView = bottom_bar_text,
saveButton = save_button
)
private val collectionCreationTabListAdapter = CollectionCreationTabListAdapter(interactor) private val collectionCreationTabListAdapter = CollectionCreationTabListAdapter(interactor)
private val collectionSaveListAdapter = SaveCollectionListAdapter(interactor) private val collectionSaveListAdapter = SaveCollectionListAdapter(interactor)
private val selectTabsConstraints = ConstraintSet() private val selectTabsConstraints = ConstraintSet()
@ -67,8 +60,8 @@ class CollectionCreationView(
transition.duration = TRANSITION_DURATION transition.duration = TRANSITION_DURATION
transition.excludeTarget(back_button, true) transition.excludeTarget(back_button, true)
view.name_collection_edittext.filters += InputFilter.LengthFilter(COLLECTION_NAME_MAX_LENGTH) name_collection_edittext.filters += InputFilter.LengthFilter(COLLECTION_NAME_MAX_LENGTH)
view.name_collection_edittext.setOnEditorActionListener { view, actionId, _ -> name_collection_edittext.setOnEditorActionListener { view, actionId, _ ->
val text = view.text.toString() val text = view.text.toString()
if (actionId == EditorInfo.IME_ACTION_DONE && text.isNotBlank()) { if (actionId == EditorInfo.IME_ACTION_DONE && text.isNotBlank()) {
when (step) { when (step) {
@ -82,13 +75,13 @@ class CollectionCreationView(
false false
} }
view.tab_list.run { tab_list.run {
adapter = collectionCreationTabListAdapter adapter = collectionCreationTabListAdapter
itemAnimator = null itemAnimator = null
layoutManager = LinearLayoutManager(containerView.context, RecyclerView.VERTICAL, true) layoutManager = LinearLayoutManager(containerView.context, RecyclerView.VERTICAL, true)
} }
view.collections_list.run { collections_list.run {
adapter = collectionSaveListAdapter adapter = collectionSaveListAdapter
layoutManager = LinearLayoutManager(containerView.context, RecyclerView.VERTICAL, true) layoutManager = LinearLayoutManager(containerView.context, RecyclerView.VERTICAL, true)
} }
@ -98,6 +91,7 @@ class CollectionCreationView(
cacheState(state) cacheState(state)
bottomBarView.update(step, state)
when (step) { when (step) {
SaveCollectionStep.SelectTabs -> updateForSelectTabs(state) SaveCollectionStep.SelectTabs -> updateForSelectTabs(state)
SaveCollectionStep.SelectCollection -> updateForSelectCollection() SaveCollectionStep.SelectCollection -> updateForSelectCollection()
@ -114,148 +108,90 @@ class CollectionCreationView(
selectedCollection = state.selectedTabCollection selectedCollection = state.selectedTabCollection
} }
@SuppressWarnings("ComplexMethod")
private fun updateForSelectTabs(state: CollectionCreationState) { private fun updateForSelectTabs(state: CollectionCreationState) {
view.context.components.analytics.metrics.track(Event.CollectionTabSelectOpened) containerView.context.components.analytics.metrics.track(Event.CollectionTabSelectOpened)
view.tab_list.isClickable = true tab_list.isClickable = true
back_button.setOnClickListener { back_button.apply {
interactor.onBackPressed(SaveCollectionStep.SelectTabs) text = context.getString(R.string.create_collection_select_tabs)
} setOnClickListener {
val allSelected = state.selectedTabs.size == state.tabs.size interactor.onBackPressed(SaveCollectionStep.SelectTabs)
select_all_button.text =
if (allSelected) view.context.getString(R.string.create_collection_deselect_all)
else view.context.getString(R.string.create_collection_select_all)
view.select_all_button.setOnClickListener {
if (allSelected) interactor.deselectAllTapped()
else interactor.selectAllTapped()
}
view.bottom_button_bar_layout.setOnClickListener(null)
view.bottom_button_bar_layout.isClickable = false
val drawable = AppCompatResources.getDrawable(view.context, R.drawable.ic_close)
drawable?.setTint(ContextCompat.getColor(view.context, R.color.photonWhite))
view.bottom_bar_icon_button.setImageDrawable(drawable)
view.bottom_bar_icon_button.contentDescription =
view.context.getString(R.string.create_collection_close)
view.bottom_bar_icon_button.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
view.bottom_bar_icon_button.setOnClickListener {
interactor.close()
}
selectTabsConstraints.clone(collection_constraint_layout)
selectTabsConstraints.applyTo(view.collection_constraint_layout)
collectionCreationTabListAdapter.updateData(state.tabs, state.selectedTabs)
back_button.text = view.context.getString(R.string.create_collection_select_tabs)
val selectTabsText = if (state.selectedTabs.isEmpty()) {
view.context.getString(R.string.create_collection_save_to_collection_empty)
} else {
view.context.getString(
if (state.selectedTabs.size == 1)
R.string.create_collection_save_to_collection_tab_selected else
R.string.create_collection_save_to_collection_tabs_selected,
state.selectedTabs.size
)
}
view.bottom_bar_text.text = selectTabsText
save_button.setOnClickListener { _ ->
if (selectedCollection != null) {
interactor.selectCollection(
collection = selectedCollection!!,
tabs = state.selectedTabs.toList()
)
} else {
interactor.saveTabsToCollection(tabs = selectedTabs.toList())
} }
} }
save_button.visibility = if (state.selectedTabs.isEmpty()) { select_all_button.apply {
View.GONE val allSelected = state.selectedTabs.size == state.tabs.size
} else { text =
View.VISIBLE if (allSelected) context.getString(R.string.create_collection_deselect_all)
else context.getString(R.string.create_collection_select_all)
setOnClickListener {
if (allSelected) interactor.deselectAllTapped()
else interactor.selectAllTapped()
}
} }
selectTabsConstraints.clone(collection_constraint_layout)
selectTabsConstraints.applyTo(collection_constraint_layout)
collectionCreationTabListAdapter.updateData(state.tabs, state.selectedTabs)
} }
private fun updateForSelectCollection() { private fun updateForSelectCollection() {
view.tab_list.isClickable = false tab_list.isClickable = false
save_button.visibility = View.GONE
view.bottom_bar_text.text =
view.context.getString(R.string.create_collection_add_new_collection)
val drawable = AppCompatResources.getDrawable(view.context, R.drawable.ic_new)
drawable?.setTint(ContextCompat.getColor(view.context, R.color.photonWhite))
view.bottom_bar_icon_button.setImageDrawable(drawable)
view.bottom_bar_icon_button.contentDescription = null
view.bottom_bar_icon_button.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
view.bottom_button_bar_layout.isClickable = true
view.bottom_button_bar_layout.setOnClickListener {
interactor.addNewCollection()
}
back_button.setOnClickListener { back_button.setOnClickListener {
interactor.onBackPressed(SaveCollectionStep.SelectCollection) interactor.onBackPressed(SaveCollectionStep.SelectCollection)
} }
TransitionManager.beginDelayedTransition( TransitionManager.beginDelayedTransition(collection_constraint_layout, transition)
view.collection_constraint_layout,
transition
)
selectCollectionConstraints.clone( selectCollectionConstraints.clone(
view.context, containerView.context,
R.layout.component_collection_creation_select_collection R.layout.component_collection_creation_select_collection
) )
selectCollectionConstraints.applyTo(view.collection_constraint_layout) selectCollectionConstraints.applyTo(collection_constraint_layout)
back_button.text =
view.context.getString(R.string.create_collection_select_collection)
} }
private fun updateForNameCollection(state: CollectionCreationState) { private fun updateForNameCollection(state: CollectionCreationState) {
view.tab_list.isClickable = false tab_list.isClickable = false
collectionCreationTabListAdapter.updateData(state.selectedTabs.toList(), state.selectedTabs, true) collectionCreationTabListAdapter.updateData(state.selectedTabs.toList(), state.selectedTabs, true)
back_button.setOnClickListener { back_button.apply {
name_collection_edittext.hideKeyboard() text = context.getString(R.string.create_collection_name_collection)
val handler = Handler() setOnClickListener {
handler.postDelayed({ name_collection_edittext.hideKeyboard()
interactor.onBackPressed(SaveCollectionStep.NameCollection) val handler = Handler()
}, TRANSITION_DURATION) handler.postDelayed({
interactor.onBackPressed(SaveCollectionStep.NameCollection)
}, TRANSITION_DURATION)
}
} }
view.name_collection_edittext.showKeyboard() name_collection_edittext.showKeyboard()
nameCollectionConstraints.clone( nameCollectionConstraints.clone(
view.context, containerView.context,
R.layout.component_collection_creation_name_collection R.layout.component_collection_creation_name_collection
) )
nameCollectionConstraints.applyTo(view.collection_constraint_layout) nameCollectionConstraints.applyTo(collection_constraint_layout)
name_collection_edittext.setText( name_collection_edittext.setText(
view.context.getString( containerView.context.getString(
R.string.create_collection_default_name, R.string.create_collection_default_name,
state.defaultCollectionNumber state.defaultCollectionNumber
) )
) )
name_collection_edittext.setSelection(0, name_collection_edittext.text.length) name_collection_edittext.setSelection(0, name_collection_edittext.text.length)
back_button.text =
view.context.getString(R.string.create_collection_name_collection)
} }
private fun updateForRenameCollection(state: CollectionCreationState) { private fun updateForRenameCollection(state: CollectionCreationState) {
view.tab_list.isClickable = false tab_list.isClickable = false
state.selectedTabCollection?.let { tabCollection -> state.selectedTabCollection?.let { tabCollection ->
val publicSuffixList = containerView.context.components.publicSuffixList
tabCollection.tabs.map { tab -> tabCollection.tabs.map { tab ->
Tab( Tab(
tab.id.toString(), sessionId = tab.id.toString(),
tab.url, url = tab.url,
tab.url.toShortUrl(view.context.components.publicSuffixList), hostname = tab.url.toShortUrl(publicSuffixList),
tab.title, title = tab.title,
mediaState = MediaState.State.NONE mediaState = MediaState.State.NONE
) )
}.let { tabs -> }.let { tabs ->
@ -263,27 +199,28 @@ class CollectionCreationView(
} }
} }
nameCollectionConstraints.clone( nameCollectionConstraints.clone(
view.context, containerView.context,
R.layout.component_collection_creation_name_collection R.layout.component_collection_creation_name_collection
) )
nameCollectionConstraints.applyTo(view.collection_constraint_layout) nameCollectionConstraints.applyTo(collection_constraint_layout)
name_collection_edittext.setText(state.selectedTabCollection?.title) name_collection_edittext.setText(state.selectedTabCollection?.title)
name_collection_edittext.setSelection(0, name_collection_edittext.text.length) name_collection_edittext.setSelection(0, name_collection_edittext.text.length)
back_button.text = back_button.apply {
view.context.getString(R.string.collection_rename) text = context.getString(R.string.collection_rename)
back_button.setOnClickListener { setOnClickListener {
name_collection_edittext.hideKeyboard() name_collection_edittext.hideKeyboard()
val handler = Handler() val handler = Handler()
handler.postDelayed({ handler.postDelayed({
interactor.onBackPressed(SaveCollectionStep.RenameCollection) interactor.onBackPressed(SaveCollectionStep.RenameCollection)
}, TRANSITION_DURATION) }, TRANSITION_DURATION)
}
} }
transition.addListener(object : Transition.TransitionListener { transition.addListener(object : Transition.TransitionListener {
override fun onTransitionStart(transition: Transition) { /* noop */ } override fun onTransitionStart(transition: Transition) { /* noop */ }
override fun onTransitionEnd(transition: Transition) { override fun onTransitionEnd(transition: Transition) {
view.name_collection_edittext.showKeyboard() name_collection_edittext.showKeyboard()
transition.removeListener(this) transition.removeListener(this)
} }
@ -291,15 +228,12 @@ class CollectionCreationView(
override fun onTransitionPause(transition: Transition) { /* noop */ } override fun onTransitionPause(transition: Transition) { /* noop */ }
override fun onTransitionResume(transition: Transition) { /* noop */ } override fun onTransitionResume(transition: Transition) { /* noop */ }
}) })
TransitionManager.beginDelayedTransition( TransitionManager.beginDelayedTransition(collection_constraint_layout, transition)
view.collection_constraint_layout,
transition
)
} }
fun onResumed() { fun onResumed() {
if (step == SaveCollectionStep.NameCollection || step == SaveCollectionStep.RenameCollection) { if (step == SaveCollectionStep.NameCollection || step == SaveCollectionStep.RenameCollection) {
view.name_collection_edittext.showKeyboard() name_collection_edittext.showKeyboard()
} }
} }

View File

@ -0,0 +1,174 @@
/* 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.collections
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
import androidx.core.view.isVisible
import io.mockk.Called
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.Tab
@RunWith(FenixRobolectricTestRunner::class)
class CollectionCreationBottomBarViewTest {
private lateinit var bottomBarView: CollectionCreationBottomBarView
private lateinit var interactor: CollectionCreationInteractor
private lateinit var layout: ViewGroup
private lateinit var iconButton: ImageButton
private lateinit var textView: TextView
private lateinit var saveButton: Button
@Before
fun setup() {
interactor = mockk(relaxed = true)
layout = mockk(relaxed = true)
iconButton = ImageButton(testContext)
textView = TextView(testContext)
saveButton = Button(testContext)
bottomBarView = CollectionCreationBottomBarView(
interactor,
layout,
iconButton,
textView,
saveButton
)
}
@Test
fun testIconButtonUpdateForSelectTabs() {
bottomBarView.update(SaveCollectionStep.SelectTabs, CollectionCreationState(0))
verify { layout.setOnClickListener(null) }
verify { layout.isClickable = false }
assertEquals("Close", iconButton.contentDescription)
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_YES, iconButton.importantForAccessibility)
iconButton.performClick()
verify { interactor.close() }
}
@Test
fun testIconButtonUpdateForSelectCollection() {
bottomBarView.update(SaveCollectionStep.SelectCollection, CollectionCreationState(0))
verify { layout wasNot Called }
assertEquals(null, iconButton.contentDescription)
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO, iconButton.importantForAccessibility)
iconButton.performClick()
verify { interactor.addNewCollection() }
}
@Test
fun testTextViewUpdateForSelectTabs() {
bottomBarView.update(
SaveCollectionStep.SelectTabs,
CollectionCreationState(
0,
selectedTabs = emptySet()
)
)
assertEquals("Select tabs to save", textView.text)
bottomBarView.update(
SaveCollectionStep.SelectTabs,
CollectionCreationState(
0,
selectedTabs = setOf(mockk())
)
)
assertEquals("1 tab selected", textView.text)
bottomBarView.update(
SaveCollectionStep.SelectTabs,
CollectionCreationState(
0,
selectedTabs = setOf(mockk(), mockk())
)
)
assertEquals("2 tabs selected", textView.text)
}
@Test
fun testTextViewUpdateForSelectCollection() {
bottomBarView.update(SaveCollectionStep.SelectCollection, CollectionCreationState(0))
assertEquals("Add new collection", textView.text)
}
@Test
fun testSaveButtonUpdateForSelectTabs() {
val collection = mockk<TabCollection>()
val tabs = setOf<Tab>(mockk(), mockk())
bottomBarView.update(
SaveCollectionStep.SelectTabs,
CollectionCreationState(
0,
selectedTabCollection = null,
selectedTabs = emptySet()
)
)
assertFalse(saveButton.isVisible)
bottomBarView.update(
SaveCollectionStep.SelectTabs,
CollectionCreationState(
0,
selectedTabCollection = collection,
selectedTabs = emptySet()
)
)
assertFalse(saveButton.isVisible)
bottomBarView.update(
SaveCollectionStep.SelectTabs,
CollectionCreationState(
0,
selectedTabCollection = null,
selectedTabs = tabs
)
)
assertTrue(saveButton.isVisible)
saveButton.performClick()
verify { interactor.saveTabsToCollection(tabs.toList()) }
bottomBarView.update(
SaveCollectionStep.SelectTabs,
CollectionCreationState(
0,
selectedTabCollection = collection,
selectedTabs = tabs
)
)
assertTrue(saveButton.isVisible)
saveButton.performClick()
verify { interactor.selectCollection(collection, tabs.toList()) }
}
@Test
fun testSaveButtonUpdateForSelectCollection() {
bottomBarView.update(SaveCollectionStep.SelectCollection, CollectionCreationState(0))
assertFalse(saveButton.isVisible)
}
}