Test migration classes (#12677)
This commit is contained in:
parent
67fda80453
commit
13949d6968
|
@ -5,28 +5,15 @@
|
||||||
package org.mozilla.fenix.migration
|
package org.mozilla.fenix.migration
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Rect
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.annotation.DimenRes
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import kotlinx.android.synthetic.main.activity_migration.*
|
import kotlinx.android.synthetic.main.activity_migration.*
|
||||||
import kotlinx.android.synthetic.main.migration_list_item.view.*
|
|
||||||
import mozilla.components.support.base.log.logger.Logger
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||||
import mozilla.components.support.migration.AbstractMigrationProgressActivity
|
import mozilla.components.support.migration.AbstractMigrationProgressActivity
|
||||||
import mozilla.components.support.migration.AbstractMigrationService
|
import mozilla.components.support.migration.AbstractMigrationService
|
||||||
import mozilla.components.support.migration.Migration
|
|
||||||
import mozilla.components.support.migration.Migration.Bookmarks
|
|
||||||
import mozilla.components.support.migration.Migration.History
|
|
||||||
import mozilla.components.support.migration.Migration.Logins
|
|
||||||
import mozilla.components.support.migration.Migration.Settings
|
|
||||||
import mozilla.components.support.migration.MigrationResults
|
import mozilla.components.support.migration.MigrationResults
|
||||||
import mozilla.components.support.migration.state.MigrationAction
|
import mozilla.components.support.migration.state.MigrationAction
|
||||||
import mozilla.components.support.migration.state.MigrationProgress
|
import mozilla.components.support.migration.state.MigrationProgress
|
||||||
|
@ -97,91 +84,10 @@ class MigrationProgressActivity : AbstractMigrationProgressActivity() {
|
||||||
migration_button.setBackgroundResource(R.drawable.migration_button_background)
|
migration_button.setBackgroundResource(R.drawable.migration_button_background)
|
||||||
migration_button_progress_bar.visibility = View.INVISIBLE
|
migration_button_progress_bar.visibility = View.INVISIBLE
|
||||||
// Keep the results list up-to-date.
|
// Keep the results list up-to-date.
|
||||||
statusAdapter.submitList(results.toItemList())
|
statusAdapter.updateData(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMigrationStateChanged(progress: MigrationProgress, results: MigrationResults) {
|
override fun onMigrationStateChanged(progress: MigrationProgress, results: MigrationResults) {
|
||||||
statusAdapter.submitList(results.toItemList())
|
statusAdapter.updateData(results)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// These are the only items we want to show migrating in the UI.
|
|
||||||
internal val whiteList = linkedMapOf(
|
|
||||||
Settings to R.string.settings_title,
|
|
||||||
History to R.string.preferences_sync_history,
|
|
||||||
Bookmarks to R.string.preferences_sync_bookmarks,
|
|
||||||
Logins to R.string.migration_text_passwords
|
|
||||||
)
|
|
||||||
|
|
||||||
internal fun MigrationResults.toItemList() = whiteList.keys
|
|
||||||
.map {
|
|
||||||
if (containsKey(it)) {
|
|
||||||
MigrationItem(it, getValue(it).success)
|
|
||||||
} else {
|
|
||||||
MigrationItem(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class MigrationItem(val migration: Migration, val status: Boolean = false)
|
|
||||||
|
|
||||||
internal class MigrationStatusAdapter :
|
|
||||||
ListAdapter<MigrationItem, MigrationStatusAdapter.ViewHolder>(DiffCallback) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
|
||||||
|
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
holder.bind(getItem(position))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int = R.layout.migration_list_item
|
|
||||||
|
|
||||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
private val context = view.context
|
|
||||||
private val title = view.migration_item_name
|
|
||||||
private val status = view.migration_status_image
|
|
||||||
|
|
||||||
fun bind(item: MigrationItem) {
|
|
||||||
// Get the resource ID for the item.
|
|
||||||
val migrationText = whiteList[item.migration]?.run {
|
|
||||||
context.getString(this)
|
|
||||||
} ?: ""
|
|
||||||
title.text = migrationText
|
|
||||||
status.visibility = if (item.status) View.VISIBLE else View.INVISIBLE
|
|
||||||
status.contentDescription = context.getString(R.string.migration_icon_description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private object DiffCallback : DiffUtil.ItemCallback<MigrationItem>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: MigrationItem, newItem: MigrationItem) =
|
|
||||||
oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: MigrationItem, newItem: MigrationItem) =
|
|
||||||
oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName &&
|
|
||||||
oldItem.status == newItem.status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class MigrationStatusItemDecoration(
|
|
||||||
@DimenRes private val spacing: Int
|
|
||||||
) : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
override fun getItemOffsets(
|
|
||||||
outRect: Rect,
|
|
||||||
view: View,
|
|
||||||
parent: RecyclerView,
|
|
||||||
state: RecyclerView.State
|
|
||||||
) {
|
|
||||||
val position = parent.getChildViewHolder(view).adapterPosition
|
|
||||||
val itemCount = state.itemCount
|
|
||||||
|
|
||||||
outRect.left = spacing
|
|
||||||
outRect.right = spacing
|
|
||||||
outRect.top = spacing
|
|
||||||
outRect.bottom = if (position == itemCount - 1) spacing else 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
/* 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.migration
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.Px
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.synthetic.main.migration_list_item.view.*
|
||||||
|
import mozilla.components.support.migration.Migration
|
||||||
|
import mozilla.components.support.migration.MigrationResults
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
|
||||||
|
internal data class MigrationItem(
|
||||||
|
val migration: Migration,
|
||||||
|
val status: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are the only items we want to show migrating in the UI.
|
||||||
|
internal val whiteList = linkedMapOf(
|
||||||
|
Migration.Settings to R.string.settings_title,
|
||||||
|
Migration.History to R.string.preferences_sync_history,
|
||||||
|
Migration.Bookmarks to R.string.preferences_sync_bookmarks,
|
||||||
|
Migration.Logins to R.string.migration_text_passwords
|
||||||
|
)
|
||||||
|
|
||||||
|
internal class MigrationStatusAdapter :
|
||||||
|
ListAdapter<MigrationItem, MigrationStatusAdapter.ViewHolder>(DiffCallback) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.migration_list_item, parent, false)
|
||||||
|
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the [results] to only include items in [whiteList] and update the adapter.
|
||||||
|
*/
|
||||||
|
fun updateData(results: MigrationResults) {
|
||||||
|
val itemList = whiteList.keys.map {
|
||||||
|
if (results.containsKey(it)) {
|
||||||
|
MigrationItem(it, results.getValue(it).success)
|
||||||
|
} else {
|
||||||
|
MigrationItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
submitList(itemList)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
private val context = view.context
|
||||||
|
private val title = view.migration_item_name
|
||||||
|
private val status = view.migration_status_image
|
||||||
|
|
||||||
|
fun bind(item: MigrationItem) {
|
||||||
|
// Get the resource ID for the item.
|
||||||
|
val migrationText = whiteList[item.migration]?.let {
|
||||||
|
context.getString(it)
|
||||||
|
}.orEmpty()
|
||||||
|
title.text = migrationText
|
||||||
|
status.isInvisible = !item.status
|
||||||
|
status.contentDescription = context.getString(R.string.migration_icon_description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object DiffCallback : DiffUtil.ItemCallback<MigrationItem>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: MigrationItem, newItem: MigrationItem) =
|
||||||
|
oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: MigrationItem, newItem: MigrationItem) =
|
||||||
|
oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName &&
|
||||||
|
oldItem.status == newItem.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MigrationStatusItemDecoration(
|
||||||
|
@Px private val spacing: Int
|
||||||
|
) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
val position = parent.getChildViewHolder(view).adapterPosition
|
||||||
|
val itemCount = state.itemCount
|
||||||
|
|
||||||
|
outRect.left = spacing
|
||||||
|
outRect.right = spacing
|
||||||
|
outRect.top = spacing
|
||||||
|
outRect.bottom = if (position == itemCount - 1) spacing else 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
/* 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
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.migration
|
package org.mozilla.fenix.migration
|
||||||
|
|
||||||
|
@ -15,7 +15,8 @@ import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
|
||||||
class MigrationTelemetryListener(
|
class MigrationTelemetryListener(
|
||||||
private val metrics: MetricController,
|
private val metrics: MetricController,
|
||||||
private val store: MigrationStore
|
private val store: MigrationStore,
|
||||||
|
private val logger: Logger = Logger("MigrationTelemetryListener")
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@ -23,7 +24,7 @@ class MigrationTelemetryListener(
|
||||||
// Observe for migration completed.
|
// Observe for migration completed.
|
||||||
store.flowScoped { flow ->
|
store.flowScoped { flow ->
|
||||||
flow.collect { state ->
|
flow.collect { state ->
|
||||||
Logger("MigrationTelemetryListener").debug("Migration state: ${state.progress}")
|
logger.debug("Migration state: ${state.progress}")
|
||||||
if (state.progress == MigrationProgress.COMPLETED) {
|
if (state.progress == MigrationProgress.COMPLETED) {
|
||||||
metrics.track(Event.FennecToFenixMigrated)
|
metrics.track(Event.FennecToFenixMigrated)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/* 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.migration
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import kotlinx.android.synthetic.main.migration_list_item.view.*
|
||||||
|
import mozilla.components.support.migration.Migration
|
||||||
|
import mozilla.components.support.migration.MigrationRun
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class MigrationStatusAdapterTest {
|
||||||
|
|
||||||
|
private lateinit var adapter: MigrationStatusAdapter
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
adapter = MigrationStatusAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getItemCount should return the number of items in whitelist`() {
|
||||||
|
assertEquals(0, adapter.itemCount)
|
||||||
|
|
||||||
|
adapter.updateData(mapOf(
|
||||||
|
Migration.Addons to MigrationRun(0, success = true),
|
||||||
|
Migration.Settings to MigrationRun(0, success = true),
|
||||||
|
Migration.Bookmarks to MigrationRun(0, success = false)
|
||||||
|
))
|
||||||
|
assertEquals(4, adapter.itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `creates and binds viewholder`() {
|
||||||
|
adapter.updateData(mapOf(
|
||||||
|
Migration.History to MigrationRun(0, success = true)
|
||||||
|
))
|
||||||
|
|
||||||
|
val holder1 = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||||
|
val holder2 = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||||
|
adapter.bindViewHolder(holder1, 0)
|
||||||
|
adapter.bindViewHolder(holder2, 1)
|
||||||
|
|
||||||
|
assertEquals("Settings", holder1.itemView.migration_item_name.text)
|
||||||
|
assertEquals(View.INVISIBLE, holder1.itemView.migration_status_image.visibility)
|
||||||
|
|
||||||
|
assertEquals("History", holder2.itemView.migration_item_name.text)
|
||||||
|
assertEquals(View.VISIBLE, holder2.itemView.migration_status_image.visibility)
|
||||||
|
assertEquals("Migration completed", holder2.itemView.migration_status_image.contentDescription)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/* 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.migration
|
||||||
|
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.verify
|
||||||
|
import io.mockk.verifyOrder
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import mozilla.components.support.migration.state.MigrationAction
|
||||||
|
import mozilla.components.support.migration.state.MigrationStore
|
||||||
|
import mozilla.components.support.test.ext.joinBlocking
|
||||||
|
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
class MigrationTelemetryListenerTest {
|
||||||
|
|
||||||
|
private val testDispatcher = TestCoroutineDispatcher()
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
|
||||||
|
|
||||||
|
@MockK(relaxed = true) private lateinit var metrics: MetricController
|
||||||
|
@MockK(relaxed = true) private lateinit var logger: Logger
|
||||||
|
private lateinit var store: MigrationStore
|
||||||
|
private lateinit var listener: MigrationTelemetryListener
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
store = MigrationStore()
|
||||||
|
listener = MigrationTelemetryListener(
|
||||||
|
metrics = metrics,
|
||||||
|
store = store,
|
||||||
|
logger = logger
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `progress state is logged`() = testDispatcher.runBlockingTest {
|
||||||
|
listener.start()
|
||||||
|
store.dispatch(MigrationAction.Started).joinBlocking()
|
||||||
|
store.dispatch(MigrationAction.Completed).joinBlocking()
|
||||||
|
store.dispatch(MigrationAction.Clear).joinBlocking()
|
||||||
|
|
||||||
|
verifyOrder {
|
||||||
|
logger.debug("Migration state: MIGRATING")
|
||||||
|
logger.debug("Migration state: COMPLETED")
|
||||||
|
logger.debug("Migration state: NONE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `metrics are logged when migration is completed`() = testDispatcher.runBlockingTest {
|
||||||
|
listener.start()
|
||||||
|
store.dispatch(MigrationAction.Completed).joinBlocking()
|
||||||
|
|
||||||
|
verify { metrics.track(Event.FennecToFenixMigrated) }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user