summaryrefslogtreecommitdiffhomepage
path: root/android/app/src/androidTest
diff options
context:
space:
mode:
authorKalle Lindström <karl.lindstrom@mullvad.net>2025-04-15 15:59:12 +0200
committerKalle Lindström <karl.lindstrom@mullvad.net>2025-04-22 12:12:12 +0200
commit926468949b2facb49182c1a5a7597b720f9e5cc7 (patch)
tree30950262e3f6adb784a12d89f8c8def37a604356 /android/app/src/androidTest
parent3a58848059a1fb8c429a49acff9e2f57b1d91fd2 (diff)
downloadmullvadvpn-926468949b2facb49182c1a5a7597b720f9e5cc7.tar.xz
mullvadvpn-926468949b2facb49182c1a5a7597b720f9e5cc7.zip
Implement manage devices screen
Diffstat (limited to 'android/app/src/androidTest')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreenTest.kt4
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreenTest.kt157
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/util/Matcher.kt12
3 files changed, 171 insertions, 2 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreenTest.kt
index 88b783df83..6f7b848e8a 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreenTest.kt
@@ -41,9 +41,9 @@ class AccountScreenTest {
onManageAccountClick: () -> Unit = {},
onLogoutClick: () -> Unit = {},
onPurchaseBillingProductClick: (productId: ProductId) -> Unit = {},
- navigateToDeviceInfo: () -> Unit = {},
navigateToVerificationPendingDialog: () -> Unit = {},
onBackClick: () -> Unit = {},
+ onManageDevicesClick: () -> Unit = {},
) {
setContentWithTheme {
AccountScreen(
@@ -53,9 +53,9 @@ class AccountScreenTest {
onManageAccountClick = onManageAccountClick,
onLogoutClick = onLogoutClick,
onPurchaseBillingProductClick = onPurchaseBillingProductClick,
- navigateToDeviceInfo = navigateToDeviceInfo,
navigateToVerificationPendingDialog = navigateToVerificationPendingDialog,
onBackClick = onBackClick,
+ onManageDevicesClick = onManageDevicesClick,
)
}
}
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreenTest.kt
new file mode 100644
index 0000000000..a54064b885
--- /dev/null
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreenTest.kt
@@ -0,0 +1,157 @@
+package net.mullvad.mullvadvpn.compose.screen
+
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import de.mannodermaus.junit5.compose.ComposeContext
+import io.mockk.MockKAnnotations
+import io.mockk.mockk
+import io.mockk.verify
+import java.time.ZonedDateTime
+import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
+import net.mullvad.mullvadvpn.compose.setContentWithTheme
+import net.mullvad.mullvadvpn.compose.state.ManageDevicesItemUiState
+import net.mullvad.mullvadvpn.compose.state.ManageDevicesUiState
+import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
+import net.mullvad.mullvadvpn.compose.util.withRole
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.DeviceId
+import net.mullvad.mullvadvpn.lib.model.GetDeviceListError
+import net.mullvad.mullvadvpn.util.Lce
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+
+@ExperimentalTestApi
+class ManageDevicesScreenTest {
+ @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension()
+
+ @BeforeEach
+ fun setup() {
+ MockKAnnotations.init(this)
+ }
+
+ private fun ComposeContext.initScreen(
+ state: Lce<ManageDevicesUiState, GetDeviceListError>,
+ snackbarHostState: SnackbarHostState = SnackbarHostState(),
+ onBackClick: () -> Unit = {},
+ onTryAgainClicked: () -> Unit = {},
+ navigateToRemoveDeviceConfirmationDialog: (device: Device) -> Unit = {},
+ ) {
+ setContentWithTheme {
+ ManageDevicesScreen(
+ state = state,
+ snackbarHostState = snackbarHostState,
+ onBackClick = onBackClick,
+ onTryAgainClicked = onTryAgainClicked,
+ navigateToRemoveDeviceConfirmationDialog = navigateToRemoveDeviceConfirmationDialog,
+ )
+ }
+ }
+
+ @Test
+ fun loadingStateShowsProgressIndicator() {
+ composeExtension.use {
+ // Arrange
+ initScreen(state = Lce.Loading)
+
+ // Assert
+ onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR).assertIsDisplayed()
+ }
+ }
+
+ @Test
+ fun errorStateShowsErrorMessageAndTryAgainButton() {
+ composeExtension.use {
+ // Arrange
+ val onTryAgainClicked: () -> Unit = mockk(relaxed = true)
+ initScreen(
+ state = Lce.Error(GetDeviceListError.Unknown(Throwable("error"))),
+ onTryAgainClicked = onTryAgainClicked,
+ )
+
+ // Assert
+ onNodeWithText("Failed to fetch list of devices").assertIsDisplayed()
+ onNodeWithText("Try again").assertIsDisplayed()
+ onNodeWithText("Manage devices").assertIsDisplayed()
+
+ // Act
+ onNodeWithText("Try again").performClick()
+
+ // Assert
+ verify(exactly = 1) { onTryAgainClicked.invoke() }
+ }
+ }
+
+ @Test
+ fun contentStateShowsDeviceListCorrectly() {
+ composeExtension.use {
+ // Arrange
+ val device1 =
+ Device(
+ id = DeviceId.fromString("12345678-1234-5678-1234-567812345678"),
+ name = "Laptop",
+ creationDate = ZonedDateTime.now().minusSeconds(100),
+ )
+ val device2 =
+ Device(
+ id = DeviceId.fromString("87654321-1234-5678-1234-567812345678"),
+ name = "My Phone",
+ creationDate = ZonedDateTime.now().minusSeconds(200),
+ )
+
+ val device3 =
+ Device(
+ id = DeviceId.fromString("87654321-4321-5678-1234-567812345678"),
+ name = "Tablet",
+ creationDate = ZonedDateTime.now().minusSeconds(300),
+ )
+
+ val state =
+ ManageDevicesUiState(
+ devices =
+ listOf(
+ ManageDevicesItemUiState(
+ device2,
+ isLoading = false,
+ isCurrentDevice = true,
+ ),
+ ManageDevicesItemUiState(
+ device1,
+ isLoading = false,
+ isCurrentDevice = false,
+ ),
+ ManageDevicesItemUiState(
+ device3,
+ isLoading = true,
+ isCurrentDevice = false,
+ ),
+ )
+ )
+ initScreen(state = Lce.Content(state))
+
+ // Assert
+ onNodeWithText("Manage devices").assertIsDisplayed()
+
+ onNodeWithText("Laptop").assertIsDisplayed()
+ onNodeWithText("Current device").assertIsDisplayed()
+ onNodeWithText("My Phone").assertIsDisplayed()
+ onNodeWithText("Tablet").assertIsDisplayed()
+
+ // We should have 2 visible buttons (the navbar back button and the remove button for
+ // device "Laptop"
+ val buttons = onAllNodes(withRole(Role.Button))
+ buttons.assertCountEquals(2)
+ buttons[0].assertIsDisplayed()
+ buttons[1].assertIsDisplayed()
+
+ // Make sure the device that is loading is displaying the spinner
+ onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR).assertIsDisplayed()
+ }
+ }
+}
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/util/Matcher.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/util/Matcher.kt
new file mode 100644
index 0000000000..636084a335
--- /dev/null
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/util/Matcher.kt
@@ -0,0 +1,12 @@
+package net.mullvad.mullvadvpn.compose.util
+
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.SemanticsMatcher
+
+fun withRole(role: Role): SemanticsMatcher =
+ SemanticsMatcher("${SemanticsProperties.Role.name} == '$role'") {
+ val roleProperty = it.config.getOrNull(SemanticsProperties.Role) ?: false
+ roleProperty == role
+ }