Skip to content
This repository has been archived by the owner on Feb 20, 2023. It is now read-only.

Commit

Permalink
Test migration classes (#12677)
Browse files Browse the repository at this point in the history
  • Loading branch information
NotWoods committed Jul 17, 2020
1 parent 67fda80 commit 13949d6
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,15 @@
package org.mozilla.fenix.migration

import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DimenRes
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
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.migration_list_item.view.*
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.migration.AbstractMigrationProgressActivity
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.state.MigrationAction
import mozilla.components.support.migration.state.MigrationProgress
Expand Down Expand Up @@ -97,91 +84,10 @@ class MigrationProgressActivity : AbstractMigrationProgressActivity() {
migration_button.setBackgroundResource(R.drawable.migration_button_background)
migration_button_progress_bar.visibility = View.INVISIBLE
// Keep the results list up-to-date.
statusAdapter.submitList(results.toItemList())
statusAdapter.updateData(results)
}

override fun onMigrationStateChanged(progress: MigrationProgress, results: MigrationResults) {
statusAdapter.submitList(results.toItemList())
}
}

// 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
statusAdapter.updateData(results)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* 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/. */
* 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

Expand All @@ -15,15 +15,16 @@ import org.mozilla.fenix.components.metrics.MetricController

class MigrationTelemetryListener(
private val metrics: MetricController,
private val store: MigrationStore
private val store: MigrationStore,
private val logger: Logger = Logger("MigrationTelemetryListener")
) {

@OptIn(ExperimentalCoroutinesApi::class)
fun start() {
// Observe for migration completed.
store.flowScoped { flow ->
flow.collect { state ->
Logger("MigrationTelemetryListener").debug("Migration state: ${state.progress}")
logger.debug("Migration state: ${state.progress}")
if (state.progress == MigrationProgress.COMPLETED) {
metrics.track(Event.FennecToFenixMigrated)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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) }
}
}

0 comments on commit 13949d6

Please sign in to comment.