summaryrefslogtreecommitdiffhomepage
path: root/android/app/src/androidTest
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2021-10-13 18:14:45 +0200
committerAlbin <albin@mullvad.net>2021-12-16 11:38:25 +0100
commit6c4d31dfc59772e9a80d0260fee34cf03d5c05f5 (patch)
tree8418020b852073a6dfb0c4c44fabdb1586404d11 /android/app/src/androidTest
parent4dcdb1306d23a8e831389130fdef452cae4dc809 (diff)
downloadmullvadvpn-6c4d31dfc59772e9a80d0260fee34cf03d5c05f5.tar.xz
mullvadvpn-6c4d31dfc59772e9a80d0260fee34cf03d5c05f5.zip
Split Android project and app module
The purpose of this is to: * Comply better with the default Android project structure (see https://developer.android.com/studio/build). * Avoid conflicts between project and app dependencies and plugins.
Diffstat (limited to 'android/app/src/androidTest')
-rw-r--r--android/app/src/androidTest/AndroidManifest.xml8
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/RecyclerViewMatcher.kt52
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/ipc/HandlerFlowTest.kt48
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/ui/fragments/SplitTunnelingFragmentTest.kt145
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))
+ }
+ }
+}