diff options
Diffstat (limited to 'android/app/src/androidTest')
4 files changed, 253 insertions, 0 deletions
diff --git a/android/app/src/androidTest/AndroidManifest.xml b/android/app/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..41b8daf8c8 --- /dev/null +++ b/android/app/src/androidTest/AndroidManifest.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="net.mullvad.mullvadvpn.test"> + + <!-- Required on certain Android versions and/or ABIs + https://github.com/mockk/mockk/issues/297#issuecomment-641361770 --> + <application android:extractNativeLibs="true" /> +</manifest> diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/RecyclerViewMatcher.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/RecyclerViewMatcher.kt new file mode 100644 index 0000000000..2e87df9720 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/RecyclerViewMatcher.kt @@ -0,0 +1,52 @@ +package net.mullvad.mullvadvpn + +import android.content.res.Resources +import android.content.res.Resources.NotFoundException +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher + +class RecyclerViewMatcher(private val recyclerViewId: Int) { + fun atPosition(position: Int): Matcher<View> { + return atPositionOnView(position) + } + + fun atPositionOnView(position: Int, targetViewId: Int? = null): Matcher<View> = + object : TypeSafeMatcher<View>() { + var resources: Resources? = null + var childView: View? = null + + override fun describeTo(description: Description) { + val idDescription = resources?.let { + try { + it.getResourceName(recyclerViewId) + } catch (var4: NotFoundException) { + "$recyclerViewId (resource name not found)" + } + } ?: recyclerViewId.toString() + description.appendText("with id: $idDescription") + } + + override fun matchesSafely(view: View): Boolean { + resources = view.resources + val recyclerView = + view.rootView.findViewById<View>(recyclerViewId) as RecyclerView? + if (recyclerView == null || recyclerView.id != recyclerViewId) { + return false + } + childView = recyclerView.findViewHolderForAdapterPosition(position)?.itemView + val targetView = targetViewId?.let { id -> + childView?.findViewById<View>(id) + } ?: childView + return view == targetView + } + } + + companion object { + fun withRecyclerView(recyclerViewId: Int): RecyclerViewMatcher { + return RecyclerViewMatcher(recyclerViewId) + } + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/ipc/HandlerFlowTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/ipc/HandlerFlowTest.kt new file mode 100644 index 0000000000..709f330b0d --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/ipc/HandlerFlowTest.kt @@ -0,0 +1,48 @@ +package net.mullvad.mullvadvpn.ipc + +import android.os.Bundle +import android.os.Looper +import android.os.Message +import android.os.Parcelable +import kotlin.test.assertEquals +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import kotlinx.parcelize.Parcelize +import org.junit.Test + +class HandlerFlowTest { + val looper by lazy { Looper.getMainLooper() } + + val handler: HandlerFlow<Data?> by lazy { + HandlerFlow(looper) { message -> + message.data.getParcelable(DATA_KEY) + } + } + + @Test + fun test_message_extraction() { + sendMessage(Data(1)) + sendMessage(Data(2)) + sendMessage(Data(3)) + + val extractedData = runBlocking { handler.take(3).toList() } + + assertEquals(listOf(Data(1), Data(2), Data(3)), extractedData) + } + + private fun sendMessage(messageData: Data) { + val message = Message().apply { + data = Bundle().apply { putParcelable(DATA_KEY, messageData) } + } + + handler.handleMessage(message) + } + + companion object { + const val DATA_KEY = "data" + + @Parcelize + data class Data(val id: Int) : Parcelable + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/ui/fragments/SplitTunnelingFragmentTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/ui/fragments/SplitTunnelingFragmentTest.kt new file mode 100644 index 0000000000..8bd3cc70b8 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/ui/fragments/SplitTunnelingFragmentTest.kt @@ -0,0 +1,145 @@ +package net.mullvad.mullvadvpn.ui.fragments + +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.lifecycle.Lifecycle +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerifyAll +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkClass +import io.mockk.unmockkAll +import io.mockk.verifyAll +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.runBlocking +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.RecyclerViewMatcher.Companion.withRecyclerView +import net.mullvad.mullvadvpn.applist.ViewIntent +import net.mullvad.mullvadvpn.di.APPS_SCOPE +import net.mullvad.mullvadvpn.model.ListItemData +import net.mullvad.mullvadvpn.model.WidgetState +import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.core.qualifier.named +import org.koin.core.scope.Scope +import org.koin.test.KoinTest +import org.koin.test.mock.MockProviderRule +import org.koin.test.mock.declareMock + +@RunWith(AndroidJUnit4::class) +@LargeTest +class SplitTunnelingFragmentTest : KoinTest { + + private val mockedViewModel = mockk<SplitTunnelingViewModel>(relaxUnitFun = true) + private val sharedFlow = MutableSharedFlow<List<ListItemData>>() + private val scope: Scope = getKoin().getOrCreateScope(APPS_SCOPE, named(APPS_SCOPE)) + + @get:Rule + val mockProvider = MockProviderRule.create { clazz -> + when (clazz) { + SplitTunnelingViewModel::class -> mockedViewModel + else -> mockkClass(clazz) + } + } + + @Before + fun setUp() { + scope.declareMock<SplitTunnelingViewModel>() + every { mockedViewModel.listItems } returns sharedFlow + coEvery { mockedViewModel.processIntent(ViewIntent.ViewIsReady) } just Runs + } + + @After + fun tearDown() { + scope.close() + unmockkAll() + } + + @Test + fun test_fragment_title() { + launchFragmentInContainer<SplitTunnelingFragment>(themeResId = R.style.AppTheme) + + onView(withId(R.id.collapsing_toolbar)) + .check(matches(withContentDescription("Split tunneling"))) + } + + @Test + fun test_fragment_loading() { + val scenario = launchFragmentInContainer<SplitTunnelingFragment>( + themeResId = R.style.AppTheme, initialState = Lifecycle.State.CREATED + ) + scenario.moveToState(Lifecycle.State.RESUMED) + sharedFlow.tryEmit(emptyList()) + + verifyAll { + mockedViewModel.listItems + } + } + + @Test + fun test_fragment_list_displayed() = runBlocking { + launchFragmentInContainer<SplitTunnelingFragment>( + themeResId = R.style.AppTheme, initialState = Lifecycle.State.RESUMED + ) + + sharedFlow.emit( + listOf( + ListItemData.build("testItem") { + type = ListItemData.PLAIN + text = "Test Item" + action = ListItemData.ItemAction(text.toString()) + } + ) + ) + + onView(withRecyclerView(R.id.recyclerView).atPositionOnView(0, R.id.plain_text)) + .check(matches(withText("Test Item"))) + + verifyAll { + mockedViewModel.listItems + } + } + + @Test + fun test_fragment_list_click_application_item() = runBlocking { + val testListItem = ListItemData.build("test.package.name") { + type = ListItemData.APPLICATION + text = "Test App Name" + action = ListItemData.ItemAction("test.package.name") + widget = WidgetState.ImageState(R.drawable.ic_icons_add) + } + + coEvery { + mockedViewModel.processIntent(ViewIntent.ChangeApplicationGroup(testListItem)) + } just Runs + + launchFragmentInContainer<SplitTunnelingFragment>( + themeResId = R.style.AppTheme, initialState = Lifecycle.State.RESUMED + ) + + sharedFlow.emit(listOf(testListItem)) + + onView(withRecyclerView(R.id.recyclerView).atPositionOnView(0, R.id.itemText)) + .check(matches(withText("Test App Name"))) + + onView(withRecyclerView(R.id.recyclerView).atPosition(0)).perform(click()) + + coVerifyAll { + mockedViewModel.listItems + mockedViewModel.processIntent(ViewIntent.ChangeApplicationGroup(testListItem)) + } + } +} |
