diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt index c3686d119..f794eee99 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt @@ -8,6 +8,7 @@ import android.content.Intent import android.content.Intent.ACTION_SEND import android.content.Intent.EXTRA_TEXT import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.navigation.NavController @@ -53,10 +54,8 @@ class DefaultShareController( } override fun handleShareToApp(app: AppShareOption) { - val shareText = tabs.joinToString("\n") { tab -> tab.url } - val intent = Intent(ACTION_SEND).apply { - putExtra(EXTRA_TEXT, shareText) + putExtra(EXTRA_TEXT, getShareText()) type = "text/plain" flags = FLAG_ACTIVITY_NEW_TASK setClassName(app.packageName, app.activityName) @@ -91,7 +90,8 @@ class DefaultShareController( dismiss() } - private fun sendTab(deviceId: String) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun sendTab(deviceId: String) { account?.run { tabs.forEach { tab -> deviceConstellation().sendEventToDeviceAsync( @@ -101,4 +101,7 @@ class DefaultShareController( } } } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun getShareText() = tabs.joinToString("\n") { tab -> tab.url } } diff --git a/app/src/main/java/org/mozilla/fenix/share/viewholders/AccountDeviceViewHolder.kt b/app/src/main/java/org/mozilla/fenix/share/viewholders/AccountDeviceViewHolder.kt index de61568db..9d2447818 100644 --- a/app/src/main/java/org/mozilla/fenix/share/viewholders/AccountDeviceViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/share/viewholders/AccountDeviceViewHolder.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.share.viewholders import android.content.Context import android.graphics.PorterDuff import android.view.View +import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.account_share_list_item.view.* @@ -17,7 +18,8 @@ import org.mozilla.fenix.share.listadapters.SyncShareOption class AccountDeviceViewHolder( itemView: View, - private val interactor: ShareToAccountDevicesInteractor + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + val interactor: ShareToAccountDevicesInteractor ) : RecyclerView.ViewHolder(itemView) { private val context: Context = itemView.context diff --git a/app/src/main/java/org/mozilla/fenix/share/viewholders/AppViewHolder.kt b/app/src/main/java/org/mozilla/fenix/share/viewholders/AppViewHolder.kt index 1e2423182..02787247b 100644 --- a/app/src/main/java/org/mozilla/fenix/share/viewholders/AppViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/share/viewholders/AppViewHolder.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.share.viewholders import android.view.View +import androidx.annotation.VisibleForTesting import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.app_share_list_item.view.* import org.mozilla.fenix.R @@ -13,7 +14,8 @@ import org.mozilla.fenix.share.listadapters.AppShareOption class AppViewHolder( itemView: View, - interactor: ShareToAppsInteractor + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + val interactor: ShareToAppsInteractor ) : RecyclerView.ViewHolder(itemView) { private var application: AppShareOption? = null diff --git a/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt b/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt new file mode 100644 index 000000000..e53c201f1 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt @@ -0,0 +1,190 @@ +/* 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.share + +import android.content.Intent +import androidx.fragment.app.Fragment +import androidx.navigation.NavController +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isTrue +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import io.mockk.verifyOrder +import kotlinx.coroutines.ObsoleteCoroutinesApi +import mozilla.components.concept.sync.Device +import mozilla.components.concept.sync.DeviceEventOutgoing +import mozilla.components.concept.sync.DeviceType +import mozilla.components.concept.sync.OAuthAccount +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.TestApplication +import org.mozilla.fenix.ext.nav +import org.mozilla.fenix.share.listadapters.AppShareOption +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@UseExperimental(ObsoleteCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +@Config(application = TestApplication::class) +class ShareControllerTest { + private val fragment = mockk(relaxed = true) + private val tabsToShare = listOf( + ShareTab("url0", "title0", "sessionId0"), + ShareTab("url1", "title1") + ) + private val textToShare = "${tabsToShare[0].url}\n${tabsToShare[1].url}" + private val account = mockk(relaxed = true) + private val navController = mockk(relaxed = true) + private val dismiss = mockk<() -> Unit>(relaxed = true) + // Use a spy that allows overriding "controller.sendTab below" + private val controller = spyk(DefaultShareController(fragment, tabsToShare, account, navController, dismiss)) + + @Test + fun `handleShareClosed should call a passed in delegate to close this`() { + controller.handleShareClosed() + + verify { dismiss() } + } + + @Test + fun `handleShareToApp should start a new sharing activity and close this`() { + val appPackageName = "package" + val appClassName = "activity" + val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) + val shareIntent = slot() + every { fragment.startActivity(capture(shareIntent)) } just Runs + + controller.handleShareToApp(appShareOption) + + // Check that the Intent used for querying apps has the expected structre + assertAll { + assertThat(shareIntent.isCaptured).isTrue() + assertThat(shareIntent.captured.action).isEqualTo(Intent.ACTION_SEND) + assertThat(shareIntent.captured.extras!![Intent.EXTRA_TEXT]).isEqualTo(textToShare) + assertThat(shareIntent.captured.type).isEqualTo("text/plain") + assertThat(shareIntent.captured.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK) + assertThat(shareIntent.captured.component!!.packageName).isEqualTo(appPackageName) + assertThat(shareIntent.captured.component!!.className).isEqualTo(appClassName) + } + verifyOrder { + fragment.startActivity(shareIntent.captured) + dismiss() + } + } + + @Test + @Suppress("DeferredResultUnused") + fun `handleShareToDevice should share to account device, inform callbacks and dismiss`() { + val deviceToShareTo = + Device("deviceId", "deviceName", DeviceType.UNKNOWN, false, 0L, emptyList(), false, null) + val tabSharedCallbackActivity = mockk(relaxed = true) + val sharedTabsNumber = slot() + val deviceId = slot() + every { fragment.activity } returns tabSharedCallbackActivity + + controller.handleShareToDevice(deviceToShareTo) + + // Verify all the needed methods are called. + verify { controller.sendTab(capture(deviceId)) } + verify { tabSharedCallbackActivity.onTabsShared(capture(sharedTabsNumber)) } + verify { dismiss() } + assertAll { + // sendTab() should be called for each device in the account + assertThat(deviceId.isCaptured).isTrue() + assertThat(deviceId.captured).isEqualTo(deviceToShareTo.id) + + // All current tabs should be shared + assertThat(sharedTabsNumber.isCaptured).isTrue() + assertThat(sharedTabsNumber.captured).isEqualTo(tabsToShare.size) + } + } + + @Test + fun `handleShareToAllDevices calls handleShareToDevice multiple times`() { + val devicesToShareTo = listOf( + Device("deviceId0", "deviceName0", DeviceType.UNKNOWN, false, 0L, emptyList(), false, null), + Device("deviceId1", "deviceName1", DeviceType.UNKNOWN, true, 1L, emptyList(), false, null) + ) + val tabSharedCallbackActivity = mockk(relaxed = true) + val sharedTabsNumber = slot() + val sharedToDeviceIds = mutableListOf() + every { fragment.activity } returns tabSharedCallbackActivity + + controller.handleShareToAllDevices(devicesToShareTo) + + // Verify all the needed methods are called. sendTab() should be called for each account device. + verify(exactly = devicesToShareTo.size) { controller.sendTab(capture(sharedToDeviceIds)) } + verify { tabSharedCallbackActivity.onTabsShared(capture(sharedTabsNumber)) } + verify { dismiss() } + assertAll { + // sendTab() should be called for each device in the account + assertThat(sharedToDeviceIds.size).isEqualTo(devicesToShareTo.size) + sharedToDeviceIds.forEachIndexed { index, shareToDeviceId -> + assertThat(shareToDeviceId).isEqualTo(devicesToShareTo[index].id) + } + + // All current tabs should be shared + assertThat(sharedTabsNumber.isCaptured).isTrue() + assertThat(sharedTabsNumber.captured).isEqualTo(tabsToShare.size) + } + } + + @Test + fun `handleSignIn should navigate to the Sync Fragment and dismiss this one`() { + controller.handleSignIn() + + verifyOrder { + navController.nav( + R.id.shareFragment, + ShareFragmentDirections.actionShareFragmentToTurnOnSyncFragment() + ) + dismiss() + } + } + + @Test + @Suppress("DeferredResultUnused") + fun `sendTab should send all current tabs to the selected device`() { + val deviceToShareTo = + Device("deviceId", "deviceName", DeviceType.UNKNOWN, false, 0L, emptyList(), false, null) + val sharedToDeviceIds = mutableListOf() + val outgoingEvents = mutableListOf() + + controller.sendTab(deviceToShareTo.id) + + // Verify the sync components being called and record the sent values + verify { + account.deviceConstellation() + .sendEventToDeviceAsync(capture(sharedToDeviceIds), capture(outgoingEvents)) + } + assertAll { + // All Tabs should be sent to the same device + assertThat(sharedToDeviceIds.size).isEqualTo(tabsToShare.size) + sharedToDeviceIds.forEach { sharedToDeviceId -> + assertThat(sharedToDeviceId).isEqualTo(deviceToShareTo.id) + } + // There should be an DeviceEventOutgoing.SendTab for each sent Tab + assertThat(outgoingEvents.size).isEqualTo(outgoingEvents.size) + outgoingEvents.forEachIndexed { index, event -> + assertThat((event).title).isEqualTo(tabsToShare[index].title) + assertThat((event).url).isEqualTo(tabsToShare[index].url) + } + } + } + + @Test + fun `getShareText should respect concatenate shared tabs urls`() { + assertThat(controller.getShareText()).isEqualTo(textToShare) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/share/ShareInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/share/ShareInteractorTest.kt new file mode 100644 index 000000000..c016c1745 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/share/ShareInteractorTest.kt @@ -0,0 +1,64 @@ +/* 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.share + +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.concept.sync.Device +import org.junit.Test +import org.mozilla.fenix.share.listadapters.AppShareOption + +class ShareInteractorTest { + private val controller = mockk(relaxed = true) + private val interactor = ShareInteractor(controller) + + @Test + fun onShareClosed() { + interactor.onShareClosed() + + verify { controller.handleShareClosed() } + } + + @Test + fun onSignIn() { + interactor.onSignIn() + + verify { controller.handleSignIn() } + } + + @Test + fun onAddNewDevice() { + interactor.onAddNewDevice() + + verify { controller.handleAddNewDevice() } + } + + @Test + fun onShareToDevice() { + val device = mockk() + + interactor.onShareToDevice(device) + + verify { controller.handleShareToDevice(device) } + } + + @Test + fun onSendToAllDevices() { + val devices = emptyList() + + interactor.onShareToAllDevices(devices) + + verify { controller.handleShareToAllDevices(devices) } + } + + @Test + fun onShareToApp() { + val app = mockk() + + interactor.onShareToApp(app) + + verify { controller.handleShareToApp(app) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/share/listadapters/AccountDevicesShareAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/share/listadapters/AccountDevicesShareAdapterTest.kt new file mode 100644 index 000000000..f12b25325 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/share/listadapters/AccountDevicesShareAdapterTest.kt @@ -0,0 +1,102 @@ +/* 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.share.listadapters + +import android.view.ViewGroup +import assertk.assertThat +import assertk.assertions.isEqualTo +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import io.mockk.verifyOrder +import kotlinx.coroutines.ObsoleteCoroutinesApi +import mozilla.components.support.test.robolectric.testContext +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.TestApplication +import org.mozilla.fenix.share.ShareInteractor +import org.mozilla.fenix.share.viewholders.AccountDeviceViewHolder +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@UseExperimental(ObsoleteCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +@Config(application = TestApplication::class) +class AccountDevicesShareAdapterTest { + private val syncOptions = mutableListOf(SyncShareOption.AddNewDevice, SyncShareOption.SignIn) + private val syncOptionsEmpty = mutableListOf() + private val interactor: ShareInteractor = mockk(relaxed = true) + + @Test + fun `updateData should replace all previous data with argument and call notifyDataSetChanged()`() { + // Used AccountDevicesShareAdapter as a spy to ease testing of notifyDataSetChanged() + // and syncOptionsEmpty to be able to record them being called + val adapter = spyk(AccountDevicesShareAdapter(mockk(), syncOptionsEmpty)) + every { adapter.notifyDataSetChanged() } just Runs + + adapter.updateData(syncOptions) + + verifyOrder { + syncOptionsEmpty.clear() + syncOptionsEmpty.addAll(syncOptions) + adapter.notifyDataSetChanged() + } + } + + @Test + fun `getItemCount on a default instantiated Adapter should return 0`() { + val adapter = AccountDevicesShareAdapter(mockk()) + + assertThat(adapter.itemCount).isEqualTo(0) + } + + @Test + fun `getItemCount after updateData() call should return the the passed in list's size`() { + val adapter = AccountDevicesShareAdapter(mockk(), syncOptions) + + assertThat(adapter.itemCount).isEqualTo(2) + } + + @Test + fun `the adapter uses the right ViewHolder`() { + val adapter = AccountDevicesShareAdapter(interactor) + val parentView: ViewGroup = mockk(relaxed = true) + every { parentView.context } returns testContext + + val viewHolder = adapter.onCreateViewHolder(parentView, 0) + + assertThat(viewHolder::class).isEqualTo(AccountDeviceViewHolder::class) + } + + @Test + fun `the adapter passes the Interactor to the ViewHolder`() { + val adapter = AccountDevicesShareAdapter(interactor) + val parentView: ViewGroup = mockk(relaxed = true) + every { parentView.context } returns testContext + + val viewHolder = adapter.onCreateViewHolder(parentView, 0) + + assertThat(viewHolder.interactor).isEqualTo(interactor) + } + + @Test + fun `the adapter binds the right item to a ViewHolder`() { + val adapter = AccountDevicesShareAdapter(interactor, syncOptions) + val parentView: ViewGroup = mockk(relaxed = true) + val itemView: ViewGroup = mockk(relaxed = true) + every { parentView.context } returns testContext + every { itemView.context } returns testContext + val viewHolder = spyk(AccountDeviceViewHolder(parentView, mockk())) + every { adapter.onCreateViewHolder(parentView, 0) } returns viewHolder + every { viewHolder.bind(any()) } just Runs + + adapter.bindViewHolder(viewHolder, 1) + + verify { viewHolder.bind(syncOptions[1]) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/share/listadapters/AppShareAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/share/listadapters/AppShareAdapterTest.kt new file mode 100644 index 000000000..9acb9e8cf --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/share/listadapters/AppShareAdapterTest.kt @@ -0,0 +1,105 @@ +/* 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.share.listadapters + +import android.view.ViewGroup +import assertk.assertThat +import assertk.assertions.isEqualTo +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import io.mockk.verifyOrder +import kotlinx.coroutines.ObsoleteCoroutinesApi +import mozilla.components.support.test.robolectric.testContext +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.TestApplication +import org.mozilla.fenix.share.ShareInteractor +import org.mozilla.fenix.share.viewholders.AppViewHolder +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@UseExperimental(ObsoleteCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +@Config(application = TestApplication::class) +class AppShareAdapterTest { + private val appOptions = mutableListOf( + AppShareOption("App 0", mockk(), "package 0", "activity 0"), + AppShareOption("App 1", mockk(), "package 1", "activity 1") + ) + private val appOptionsEmpty = mutableListOf() + private val interactor: ShareInteractor = mockk(relaxed = true) + + @Test + fun `updateData should replace all previous data with argument and call notifyDataSetChanged()`() { + // Used AppShareAdapter as a spy to ease testing of notifyDataSetChanged() + // and appOptionsEmpty to be able to record them being called + val adapter = spyk(AppShareAdapter(mockk(), appOptionsEmpty)) + every { adapter.notifyDataSetChanged() } just Runs + + adapter.updateData(appOptions) + + verifyOrder { + appOptionsEmpty.clear() + appOptionsEmpty.addAll(appOptions) + adapter.notifyDataSetChanged() + } + } + + @Test + fun `getItemCount on a default instantiated Adapter should return 0`() { + val adapter = AppShareAdapter(mockk()) + + assertThat(adapter.itemCount).isEqualTo(0) + } + + @Test + fun `getItemCount after updateData() call should return the the passed in list's size`() { + val adapter = AppShareAdapter(mockk(), appOptions) + + assertThat(adapter.itemCount).isEqualTo(2) + } + + @Test + fun `the adapter uses the right ViewHolder`() { + val adapter = AppShareAdapter(interactor) + val parentView: ViewGroup = mockk(relaxed = true) + every { parentView.context } returns testContext + + val viewHolder = adapter.onCreateViewHolder(parentView, 0) + + assertThat(viewHolder::class).isEqualTo(AppViewHolder::class) + } + + @Test + fun `the adapter passes the Interactor to the ViewHolder`() { + val adapter = AppShareAdapter(interactor) + val parentView: ViewGroup = mockk(relaxed = true) + every { parentView.context } returns testContext + + val viewHolder = adapter.onCreateViewHolder(parentView, 0) + + assertThat(viewHolder.interactor).isEqualTo(interactor) + } + + @Test + fun `the adapter binds the right item to a ViewHolder`() { + val adapter = AppShareAdapter(interactor, appOptions) + val parentView: ViewGroup = mockk(relaxed = true) + val itemView: ViewGroup = mockk(relaxed = true) + every { parentView.context } returns testContext + every { itemView.context } returns testContext + val viewHolder = spyk(AppViewHolder(parentView, mockk())) + every { adapter.onCreateViewHolder(parentView, 0) } returns viewHolder + every { viewHolder.bind(any()) } just Runs + + adapter.bindViewHolder(viewHolder, 1) + + verify { viewHolder.bind(appOptions[1]) } + } +}