From 595720d39c2ec554622f4d30b08c0d632024eba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20G=C3=B6ransson?= Date: Mon, 20 Nov 2023 16:25:20 +0100 Subject: [PATCH] Fix VPNSettings UI tests --- .../compose/dialog/CustomPortDialogTest.kt | 60 +++ .../compose/dialog/DnsDialogTest.kt | 125 +++++ .../compose/dialog/MtuDialogTest.kt | 153 ++++++ .../compose/screen/AccountScreenTest.kt | 10 +- .../compose/screen/ConnectScreenTest.kt | 22 +- .../compose/screen/OutOfTimeScreenTest.kt | 33 +- .../compose/screen/VpnSettingsScreenTest.kt | 457 +++++------------- .../compose/screen/WelcomeScreenTest.kt | 11 +- .../mullvadvpn/compose/component/TopBar.kt | 15 +- .../mullvadvpn/compose/dialog/DnsDialog.kt | 2 +- .../mullvadvpn/compose/dialog/MtuDialog.kt | 26 +- .../dialog/WireguardCustomPortDialog.kt | 48 +- .../mullvadvpn/compose/screen/LoginScreen.kt | 3 - .../mullvadvpn/compose/screen/MullvadApp.kt | 6 +- .../compose/screen/OutOfTimeScreen.kt | 6 +- .../compose/screen/VpnSettingsScreen.kt | 26 +- .../compose/test/ComposeTestTagConstants.kt | 4 + 17 files changed, 548 insertions(+), 459 deletions(-) create mode 100644 android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt create mode 100644 android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialogTest.kt create mode 100644 android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt new file mode 100644 index 000000000000..7f3bc5b18fee --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt @@ -0,0 +1,60 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performTextInput +import io.mockk.MockKAnnotations +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG +import net.mullvad.mullvadvpn.model.PortRange +import net.mullvad.mullvadvpn.onNodeWithTagAndText +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class CustomPortDialogTest { + @get:Rule val composeTestRule = createComposeRule() + + @Before + fun setup() { + MockKAnnotations.init(this) + } + + @SuppressLint("ComposableNaming") + @Composable + private fun testWireguardCustomPortDialog( + initialPort: Int? = null, + allowedPortRanges: List = emptyList(), + onSave: (Int?) -> Unit = { _ -> }, + onDismiss: () -> Unit = {}, + ) { + + WireguardCustomPortDialog( + initialPort = initialPort, + allowedPortRanges = allowedPortRanges, + onSave = onSave, + onDismiss = onDismiss + ) + } + + @Test + fun testShowWireguardCustomPortDialogInvalidInt() { + // Input a number to make sure that a too long number does not show and it does not crash + // the app + + // Arrange + composeTestRule.setContentWithTheme { testWireguardCustomPortDialog() } + + // Act + composeTestRule + .onNodeWithTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG) + .performTextInput("21474836471") + + // Assert + composeTestRule + .onNodeWithTagAndText(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG, "21474836471") + .assertDoesNotExist() + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialogTest.kt new file mode 100644 index 000000000000..3adfd1184a2c --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialogTest.kt @@ -0,0 +1,125 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import io.mockk.MockKAnnotations +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.viewmodel.DnsDialogViewState +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class DnsDialogTest { + @get:Rule val composeTestRule = createComposeRule() + + @Before + fun setup() { + MockKAnnotations.init(this) + } + + val defaultState = + DnsDialogViewState( + ipAddress = "", + validationResult = DnsDialogViewState.ValidationResult.Success, + isLocal = false, + isAllowLanEnabled = false, + isNewEntry = true + ) + + @SuppressLint("ComposableNaming") + @Composable + private fun testDnsDialog( + state: DnsDialogViewState = defaultState, + onDnsInputChange: (String) -> Unit = { _ -> }, + onSaveDnsClick: () -> Unit = {}, + onRemoveDnsClick: () -> Unit = {}, + onDismiss: () -> Unit = {} + ) { + DnsDialog(state, onDnsInputChange, onSaveDnsClick, onRemoveDnsClick, onDismiss) + } + + @Test + fun testDnsDialogLanWarningShownWhenLanTrafficDisabledAndLocalAddressUsed() { + // Arrange + composeTestRule.setContentWithTheme { + testDnsDialog(defaultState.copy(isAllowLanEnabled = false, isLocal = true)) + } + + // Assert + composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertExists() + } + + @Test + fun testDnsDialogLanWarningNotShownWhenLanTrafficEnabledAndLocalAddressUsed() { + // Arrange + composeTestRule.setContentWithTheme { + testDnsDialog(defaultState.copy(isAllowLanEnabled = true, isLocal = true)) + } + + // Assert + composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() + } + + @Test + fun testDnsDialogLanWarningNotShownWhenLanTrafficEnabledAndNonLocalAddressUsed() { + // Arrange + composeTestRule.setContentWithTheme { + testDnsDialog(defaultState.copy(isAllowLanEnabled = true, isLocal = false)) + } + + // Assert + composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() + } + + @Test + fun testDnsDialogLanWarningNotShownWhenLanTrafficDisabledAndNonLocalAddressUsed() { + // Arrange + composeTestRule.setContentWithTheme { + testDnsDialog(defaultState.copy(isAllowLanEnabled = false, isLocal = false)) + } + + // Assert + composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() + } + + @Test + fun testDnsDialogSubmitButtonDisabledOnInvalidDnsAddress() { + // Arrange + composeTestRule.setContentWithTheme { + testDnsDialog( + defaultState.copy( + ipAddress = "300.300.300.300", + validationResult = DnsDialogViewState.ValidationResult.InvalidAddress, + ) + ) + } + + // Assert + composeTestRule.onNodeWithText("Submit").assertIsNotEnabled() + } + + @Test + fun testDnsDialogSubmitButtonDisabledOnDuplicateDnsAddress() { + // Arrange + composeTestRule.setContentWithTheme { + testDnsDialog( + defaultState.copy( + ipAddress = "192.168.0.1", + validationResult = DnsDialogViewState.ValidationResult.DuplicateAddress, + ) + ) + } + + // Assert + composeTestRule.onNodeWithText("Submit").assertIsNotEnabled() + } + + companion object { + private const val LOCAL_DNS_SERVER_WARNING = + "The local DNS server will not work unless you enable " + + "\"Local Network Sharing\" under Preferences." + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt new file mode 100644 index 000000000000..38a3bd170da3 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt @@ -0,0 +1,153 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import io.mockk.MockKAnnotations +import io.mockk.mockk +import io.mockk.verify +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class MtuDialogTest { + @get:Rule val composeTestRule = createComposeRule() + + @Before + fun setup() { + MockKAnnotations.init(this) + } + + @SuppressLint("ComposableNaming") + @Composable + private fun testMtuDialog( + mtuInitial: Int? = null, + onSaveMtu: (Int) -> Unit = { _ -> }, + onResetMtu: () -> Unit = {}, + onDismiss: () -> Unit = {}, + ) { + MtuDialog( + mtuInitial = mtuInitial, + onSaveMtu = onSaveMtu, + onResetMtu = onResetMtu, + onDismiss = onDismiss + ) + } + + @Test + fun testMtuDialogWithDefaultValue() { + // Arrange + composeTestRule.setContentWithTheme { testMtuDialog() } + + // Assert + composeTestRule.onNodeWithText(EMPTY_STRING).assertExists() + } + + @Test + fun testMtuDialogWithEditValue() { + // Arrange + composeTestRule.setContentWithTheme { + testMtuDialog( + mtuInitial = VALID_DUMMY_MTU_VALUE, + ) + } + + // Assert + composeTestRule.onNodeWithText(VALID_DUMMY_MTU_VALUE.toString()).assertExists() + } + + @Test + fun testMtuDialogTextInput() { + // Arrange + composeTestRule.setContentWithTheme { + testMtuDialog( + null, + ) + } + + // Act + composeTestRule + .onNodeWithText(EMPTY_STRING) + .performTextInput(VALID_DUMMY_MTU_VALUE.toString()) + + // Assert + composeTestRule.onNodeWithText(VALID_DUMMY_MTU_VALUE.toString()).assertExists() + } + + @Test + fun testMtuDialogSubmitOfValidValue() { + // Arrange + val mockedSubmitHandler: (Int) -> Unit = mockk(relaxed = true) + composeTestRule.setContentWithTheme { + testMtuDialog( + VALID_DUMMY_MTU_VALUE, + onSaveMtu = mockedSubmitHandler, + ) + } + + // Act + composeTestRule.onNodeWithText("Submit").assertIsEnabled().performClick() + + // Assert + verify { mockedSubmitHandler.invoke(VALID_DUMMY_MTU_VALUE) } + } + + @Test + fun testMtuDialogSubmitButtonDisabledWhenInvalidInput() { + // Arrange + composeTestRule.setContentWithTheme { + testMtuDialog( + INVALID_DUMMY_MTU_VALUE, + ) + } + + // Assert + composeTestRule.onNodeWithText("Submit").assertIsNotEnabled() + } + + @Test + fun testMtuDialogResetClick() { + // Arrange + val mockedClickHandler: () -> Unit = mockk(relaxed = true) + composeTestRule.setContentWithTheme { + testMtuDialog( + onResetMtu = mockedClickHandler, + ) + } + + // Act + composeTestRule.onNodeWithText("Reset to default").performClick() + + // Assert + verify { mockedClickHandler.invoke() } + } + + @Test + fun testMtuDialogCancelClick() { + // Arrange + val mockedClickHandler: () -> Unit = mockk(relaxed = true) + composeTestRule.setContentWithTheme { + testMtuDialog( + onDismiss = mockedClickHandler, + ) + } + + // Assert + composeTestRule.onNodeWithText("Cancel").performClick() + + // Assert + verify { mockedClickHandler.invoke() } + } + + companion object { + private const val EMPTY_STRING = "" + private const val VALID_DUMMY_MTU_VALUE = 1337 + private const val INVALID_DUMMY_MTU_VALUE = 1111 + } +} 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 56e01a9549a2..3b42cc1c3b10 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 @@ -64,10 +64,10 @@ class AccountScreenTest { AccountScreen( uiState = AccountUiState( + showSitePayment = true, deviceName = DUMMY_DEVICE_NAME, accountNumber = DUMMY_ACCOUNT_NUMBER, accountExpiry = null, - showSitePayment = false ), uiSideEffect = MutableSharedFlow().asSharedFlow(), onManageAccountClick = mockedClickHandler @@ -196,6 +196,7 @@ class AccountScreenTest { val mockPaymentProduct: PaymentProduct = mockk() every { mockPaymentProduct.price } returns ProductPrice("$10") every { mockPaymentProduct.status } returns PaymentStatus.PENDING + val mockNavigateToVerificationPending: () -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { AccountScreen( uiState = @@ -205,6 +206,7 @@ class AccountScreenTest { PaymentState.PaymentAvailable(listOf(mockPaymentProduct)) ), uiSideEffect = MutableSharedFlow().asSharedFlow(), + navigateToVerificationPendingDialog = mockNavigateToVerificationPending ) } @@ -212,11 +214,7 @@ class AccountScreenTest { composeTestRule.onNodeWithTag(PLAY_PAYMENT_INFO_ICON_TEST_TAG).performClick() // Assert - composeTestRule - .onNodeWithText( - "We are currently verifying your purchase, this might take some time. Your time will be added if the verification is successful." - ) - .assertExists() + verify(exactly = 1) { mockNavigateToVerificationPending.invoke() } } @Test diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt index 55a7f491f807..cd25c8ce0b4b 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt @@ -18,6 +18,7 @@ import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_ACTION import net.mullvad.mullvadvpn.compose.test.RECONNECT_BUTTON_TEST_TAG import net.mullvad.mullvadvpn.compose.test.SCROLLABLE_COLUMN_TEST_TAG import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.TOP_BAR_ACCOUNT_BUTTON import net.mullvad.mullvadvpn.model.GeoIpLocation import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.relaylist.RelayItem @@ -815,28 +816,17 @@ class ConnectScreenTest { @Test fun testOpenAccountView() { - // Arrange - composeTestRule.setContentWithTheme { - ConnectScreen( - uiState = ConnectUiState.INITIAL, - ) - } - // Assert - composeTestRule.apply { onNodeWithTag(SCROLLABLE_COLUMN_TEST_TAG).assertDoesNotExist() } - } + val onAccountClickMockk: () -> Unit = mockk(relaxed = true) - @Test - fun testOpenOutOfTimeScreen() { // Arrange - val mockedOpenScreenHandler: () -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { - ConnectScreen( - uiState = ConnectUiState.INITIAL, - ) + ConnectScreen(uiState = ConnectUiState.INITIAL, onAccountClick = onAccountClickMockk) } // Assert - verify { mockedOpenScreenHandler.invoke() } + composeTestRule.onNodeWithTag(TOP_BAR_ACCOUNT_BUTTON).performClick() + + verify(exactly = 1) { onAccountClickMockk() } } } diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt index eda0a128c074..b5ecb600f63b 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt @@ -184,27 +184,6 @@ class OutOfTimeScreenTest { verify(exactly = 1) { mockClickListener.invoke() } } - @Test - fun testShowPurchaseCompleteDialog() { - // Arrange - composeTestRule.setContentWithTheme { - OutOfTimeScreen( - showSitePayment = true, - uiState = OutOfTimeUiState(), - uiSideEffect = MutableStateFlow(OutOfTimeViewModel.UiSideEffect.OpenConnectScreen), - onSitePaymentClick = {}, - onRedeemVoucherClick = {}, - onSettingsClick = {}, - onAccountClick = {}, - openConnectScreen = {}, - onPurchaseBillingProductClick = { _ -> } - ) - } - - // Assert - composeTestRule.onNodeWithText("Time was successfully added").assertExists() - } - @Test fun testShowBillingErrorPaymentButton() { // Arrange @@ -283,6 +262,7 @@ class OutOfTimeScreenTest { val mockPaymentProduct: PaymentProduct = mockk() every { mockPaymentProduct.price } returns ProductPrice("$10") every { mockPaymentProduct.status } returns PaymentStatus.PENDING + val mockNavigateToVerificationPending: () -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { OutOfTimeScreen( showSitePayment = true, @@ -292,19 +272,16 @@ class OutOfTimeScreenTest { billingPaymentState = PaymentState.PaymentAvailable(listOf(mockPaymentProduct)) ), - uiSideEffect = MutableSharedFlow().asSharedFlow() + uiSideEffect = MutableSharedFlow().asSharedFlow(), + navigateToVerificationPendingDialog = mockNavigateToVerificationPending ) } // Act composeTestRule.onNodeWithTag(PLAY_PAYMENT_INFO_ICON_TEST_TAG).performClick() + composeTestRule.onNodeWithTag(PLAY_PAYMENT_INFO_ICON_TEST_TAG).assertExists() - // Assert - composeTestRule - .onNodeWithText( - "We are currently verifying your purchase, this might take some time. Your time will be added if the verification is successful." - ) - .assertExists() + verify(exactly = 1) { mockNavigateToVerificationPending.invoke() } } @Test diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt index fa92507a3c45..49695c9c3670 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt @@ -1,7 +1,5 @@ package net.mullvad.mullvadvpn.compose.screen -import androidx.compose.ui.test.assertIsEnabled -import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription @@ -9,7 +7,6 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode -import androidx.compose.ui.test.performTextInput import io.mockk.MockKAnnotations import io.mockk.mockk import io.mockk.verify @@ -17,7 +14,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import net.mullvad.mullvadvpn.compose.setContentWithTheme import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState -import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_LAST_ITEM_TEST_TAG import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_QUANTUM_ITEM_OFF_TEST_TAG import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_QUANTUM_ITEM_ON_TEST_TAG @@ -27,6 +23,7 @@ import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_ import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_TAG import net.mullvad.mullvadvpn.model.Constraint import net.mullvad.mullvadvpn.model.Port +import net.mullvad.mullvadvpn.model.PortRange import net.mullvad.mullvadvpn.model.QuantumResistantState import net.mullvad.mullvadvpn.onNodeWithTagAndText import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem @@ -84,141 +81,6 @@ class VpnSettingsScreenTest { composeTestRule.onNodeWithText(VALID_DUMMY_MTU_VALUE).assertExists() } - @Test - fun testMtuClick() { - // Arrange - val mockedClickHandler: () -> Unit = mockk(relaxed = true) - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow(), - ) - } - - composeTestRule - .onNodeWithTag(LAZY_LIST_TEST_TAG) - .performScrollToNode(hasTestTag(LAZY_LIST_LAST_ITEM_TEST_TAG)) - - // Act - composeTestRule.onNodeWithText("WireGuard MTU").performClick() - - // Assert - verify { mockedClickHandler.invoke() } - } - - @Test - fun testMtuDialogWithDefaultValue() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText(EMPTY_STRING).assertExists() - } - - @Test - fun testMtuDialogWithEditValue() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText(VALID_DUMMY_MTU_VALUE).assertExists() - } - - @Test - fun testMtuDialogTextInput() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Act - composeTestRule.onNodeWithText(EMPTY_STRING).performTextInput(VALID_DUMMY_MTU_VALUE) - - // Assert - composeTestRule.onNodeWithText(VALID_DUMMY_MTU_VALUE).assertExists() - } - - @Test - fun testMtuDialogSubmitOfValidValue() { - // Arrange - val mockedSubmitHandler: (Int) -> Unit = mockk(relaxed = true) - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow(), - ) - } - - // Act - composeTestRule.onNodeWithText("Submit").assertIsEnabled().performClick() - - // Assert - verify { mockedSubmitHandler.invoke(VALID_DUMMY_MTU_VALUE.toInt()) } - } - - @Test - fun testMtuDialogSubmitButtonDisabledWhenInvalidInput() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText("Submit").assertIsNotEnabled() - } - - @Test - fun testMtuDialogResetClick() { - // Arrange - val mockedClickHandler: () -> Unit = mockk(relaxed = true) - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow(), - ) - } - - // Act - composeTestRule.onNodeWithText("Reset to default").performClick() - - // Assert - verify { mockedClickHandler.invoke() } - } - - @Test - fun testMtuDialogCancelClick() { - // Arrange - val mockedClickHandler: () -> Unit = mockk(relaxed = true) - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow(), - ) - } - - // Assert - composeTestRule.onNodeWithText("Cancel").performClick() - - // Assert - verify { mockedClickHandler.invoke() } - } - @Test fun testCustomDnsAddressesAndAddButtonVisibleWhenCustomDnsEnabled() { // Arrange @@ -276,6 +138,7 @@ class VpnSettingsScreenTest { uiState = VpnSettingsUiState.createDefault( isCustomDnsEnabled = true, + isLocalNetworkSharingEnabled = true, customDnsItems = listOf(CustomDnsItem(address = DUMMY_DNS_ADDRESS, isLocal = true)) ), @@ -346,136 +209,6 @@ class VpnSettingsScreenTest { } } - @Test - fun testClickAddDns() { - // Arrange - val mockedClickHandler: (Int?) -> Unit = mockk(relaxed = true) - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(isCustomDnsEnabled = true), - sideEffect = MutableSharedFlow().asSharedFlow(), - ) - } - - // Act - composeTestRule.onNodeWithText("Add a server").performClick() - - // Assert - verify { mockedClickHandler.invoke(null) } - } - - @Test - fun testShowDnsDialogForNewDnsServer() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText("Add DNS server").assertExists() - } - - @Test - fun testShowDnsDialogForUpdatingDnsServer() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText("Update DNS server").assertExists() - } - - @Test - fun testDnsDialogLanWarningShownWhenLanTrafficDisabledAndLocalAddressUsed() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertExists() - } - - @Test - fun testDnsDialogLanWarningNotShownWhenLanTrafficEnabledAndLocalAddressUsed() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() - } - - @Test - fun testDnsDialogLanWarningNotShownWhenLanTrafficEnabledAndNonLocalAddressUsed() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() - } - - @Test - fun testDnsDialogLanWarningNotShownWhenLanTrafficDisabledAndNonLocalAddressUsed() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText(LOCAL_DNS_SERVER_WARNING).assertDoesNotExist() - } - - @Test - fun testDnsDialogSubmitButtonDisabledOnInvalidDnsAddress() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText("Submit").assertIsNotEnabled() - } - - @Test - fun testDnsDialogSubmitButtonDisabledOnDuplicateDnsAddress() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText("Submit").assertIsNotEnabled() - } - @Test fun testShowSelectedTunnelQuantumOption() { // Arrange @@ -524,20 +257,6 @@ class VpnSettingsScreenTest { } } - @Test - fun testShowTunnelQuantumInfo() { - // Arrange - composeTestRule.setContentWithTheme { - VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() - ) - } - - // Assert - composeTestRule.onNodeWithText("Got it!").assertExists() - } - @Test fun testShowWireguardPortOptions() { // Arrange @@ -603,118 +322,171 @@ class VpnSettingsScreenTest { } @Test - fun testShowWireguardPortInfo() { + fun testShowWireguardCustomPort() { // Arrange composeTestRule.setContentWithTheme { VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), + uiState = + VpnSettingsUiState.createDefault( + customWireguardPort = Constraint.Only(Port(4000)) + ), sideEffect = MutableSharedFlow().asSharedFlow() ) } - // Assert + // Act composeTestRule - .onNodeWithText( - "The automatic setting will randomly choose from the valid port ranges shown below." + .onNodeWithTag(LAZY_LIST_TEST_TAG) + .performScrollToNode(hasTestTag(LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG)) + + // Assert + composeTestRule.onNodeWithText("4000").assertExists() + } + + @Test + fun testSelectWireguardCustomPort() { + // Arrange + val onWireguardPortSelected: (Constraint) -> Unit = mockk(relaxed = true) + composeTestRule.setContentWithTheme { + VpnSettingsScreen( + uiState = + VpnSettingsUiState.createDefault( + selectedWireguardPort = Constraint.Only(Port(4000)), + customWireguardPort = Constraint.Only(Port(4000)) + ), + sideEffect = MutableSharedFlow().asSharedFlow(), + onWireguardPortSelected = onWireguardPortSelected ) - .assertExists() + } + + // Act + composeTestRule + .onNodeWithTag(LAZY_LIST_TEST_TAG) + .performScrollToNode(hasTestTag(LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG)) + composeTestRule + .onNodeWithTag(testTag = LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG) + .performClick() + + // Assert + verify { onWireguardPortSelected.invoke(Constraint.Only(Port(4000))) } } + // Navigation Tests + @Test - fun testShowWireguardCustomPortDialog() { + fun testMtuClick() { // Arrange + val mockedClickHandler: (Int?) -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { VpnSettingsScreen( uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() + sideEffect = MutableSharedFlow().asSharedFlow(), + navigateToMtuDialog = mockedClickHandler ) } + composeTestRule + .onNodeWithTag(LAZY_LIST_TEST_TAG) + .performScrollToNode(hasTestTag(LAZY_LIST_LAST_ITEM_TEST_TAG)) + + // Act + composeTestRule.onNodeWithText("WireGuard MTU").performClick() + // Assert - composeTestRule.onNodeWithText("Valid ranges: 53, 120-121").assertExists() + verify { mockedClickHandler.invoke(null) } } @Test - fun testShowWireguardCustomPort() { + fun testClickAddDns() { // Arrange + val mockedClickHandler: (Int?, String?) -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { VpnSettingsScreen( - uiState = - VpnSettingsUiState.createDefault( - selectedWireguardPort = Constraint.Only(Port(4000)) - ), - sideEffect = MutableSharedFlow().asSharedFlow() + uiState = VpnSettingsUiState.createDefault(isCustomDnsEnabled = true), + sideEffect = MutableSharedFlow().asSharedFlow(), + navigateToDns = mockedClickHandler ) } // Act - composeTestRule - .onNodeWithTag(LAZY_LIST_TEST_TAG) - .performScrollToNode(hasTestTag(LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG)) + composeTestRule.onNodeWithText("Add a server").performClick() // Assert - composeTestRule.onNodeWithText("4000").assertExists() + verify { mockedClickHandler.invoke(null, null) } } @Test - fun testClickWireguardCustomPortMainCell() { + fun testShowTunnelQuantumInfo() { + val mockedShowTunnelQuantumInfoClick: () -> Unit = mockk(relaxed = true) + // Arrange - val mockOnShowCustomPortDialog: () -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { VpnSettingsScreen( uiState = VpnSettingsUiState.createDefault(), sideEffect = MutableSharedFlow().asSharedFlow(), + navigateToQuantumResistanceInfo = mockedShowTunnelQuantumInfoClick ) } // Act composeTestRule .onNodeWithTag(LAZY_LIST_TEST_TAG) - .performScrollToNode(hasTestTag(LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG)) - composeTestRule.onNodeWithTag(LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG).performClick() + .performScrollToNode(hasTestTag(LAZY_LIST_QUANTUM_ITEM_ON_TEST_TAG)) + composeTestRule.onNodeWithText("Quantum-resistant tunnel").performClick() // Assert - verify { mockOnShowCustomPortDialog.invoke() } + verify(exactly = 1) { mockedShowTunnelQuantumInfoClick() } } @Test - fun testClickWireguardCustomPortNumberCell() { + fun testShowWireguardPortInfo() { + val mockedClickHandler: (List) -> Unit = mockk(relaxed = true) + // Arrange - val mockOnShowCustomPortDialog: () -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { VpnSettingsScreen( - uiState = - VpnSettingsUiState.createDefault( - selectedWireguardPort = Constraint.Only(Port(4000)) - ), + uiState = VpnSettingsUiState.createDefault(), sideEffect = MutableSharedFlow().asSharedFlow(), + navigateToWireguardPortInfo = mockedClickHandler + ) + } + + composeTestRule.onNodeWithText("WireGuard port").performClick() + + verify(exactly = 1) { mockedClickHandler.invoke(any()) } + } + + @Test + fun testShowWireguardCustomPortDialog() { + val mockedClickHandler: () -> Unit = mockk(relaxed = true) + + // Arrange + composeTestRule.setContentWithTheme { + VpnSettingsScreen( + uiState = VpnSettingsUiState.createDefault(), + sideEffect = MutableSharedFlow().asSharedFlow(), + navigateToWireguardPortDialog = mockedClickHandler ) } - // Act composeTestRule .onNodeWithTag(LAZY_LIST_TEST_TAG) - .performScrollToNode(hasTestTag(LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG)) - composeTestRule - .onNodeWithTag(testTag = LAZY_LIST_WIREGUARD_CUSTOM_PORT_NUMBER_TEST_TAG) - .performClick() + .performScrollToNode(hasTestTag(LAZY_LIST_WIREGUARD_CUSTOM_PORT_NUMBER_TEST_TAG)) + composeTestRule.onNodeWithText("Custom").performClick() // Assert - verify { mockOnShowCustomPortDialog.invoke() } + verify(exactly = 1) { mockedClickHandler.invoke() } } @Test - fun testSelectWireguardCustomPort() { + fun testClickWireguardCustomPortMainCell() { // Arrange - val onWireguardPortSelected: (Constraint) -> Unit = mockk(relaxed = true) + val mockOnShowCustomPortDialog: () -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { VpnSettingsScreen( - uiState = - VpnSettingsUiState.createDefault( - selectedWireguardPort = Constraint.Only(Port(4000)) - ), + uiState = VpnSettingsUiState.createDefault(), sideEffect = MutableSharedFlow().asSharedFlow(), - onWireguardPortSelected = onWireguardPortSelected + navigateToWireguardPortDialog = mockOnShowCustomPortDialog ) } @@ -722,45 +494,44 @@ class VpnSettingsScreenTest { composeTestRule .onNodeWithTag(LAZY_LIST_TEST_TAG) .performScrollToNode(hasTestTag(LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG)) - composeTestRule - .onNodeWithTag(testTag = LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG) - .performClick() + composeTestRule.onNodeWithTag(LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG).performClick() // Assert - verify { onWireguardPortSelected.invoke(Constraint.Only(Port(4000))) } + verify { mockOnShowCustomPortDialog.invoke() } } @Test - fun testShowWireguardCustomPortDialogInvalidInt() { - // Input a number to make sure that a too long number does not show and it does not crash - // the app - + fun testClickWireguardCustomPortNumberCell() { // Arrange + val mockOnShowCustomPortDialog: () -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { VpnSettingsScreen( - uiState = VpnSettingsUiState.createDefault(), - sideEffect = MutableSharedFlow().asSharedFlow() + uiState = + VpnSettingsUiState.createDefault( + selectedWireguardPort = Constraint.Only(Port(4000)) + ), + sideEffect = MutableSharedFlow().asSharedFlow(), + navigateToWireguardPortDialog = mockOnShowCustomPortDialog ) } // Act composeTestRule - .onNodeWithTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG) - .performTextInput("21474836471") + .onNodeWithTag(LAZY_LIST_TEST_TAG) + .performScrollToNode(hasTestTag(LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG)) + composeTestRule + .onNodeWithTag(testTag = LAZY_LIST_WIREGUARD_CUSTOM_PORT_NUMBER_TEST_TAG) + .performClick() // Assert - composeTestRule - .onNodeWithTagAndText(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG, "21474836471") - .assertDoesNotExist() + verify { mockOnShowCustomPortDialog.invoke() } } companion object { private const val LOCAL_DNS_SERVER_WARNING = "The local DNS server will not work unless you enable " + "\"Local Network Sharing\" under Preferences." - private const val EMPTY_STRING = "" private const val VALID_DUMMY_MTU_VALUE = "1337" - private const val INVALID_DUMMY_MTU_VALUE = "1111" private const val DUMMY_DNS_ADDRESS = "0.0.0.1" private const val DUMMY_DNS_ADDRESS_2 = "0.0.0.2" private const val DUMMY_DNS_ADDRESS_3 = "0.0.0.3" diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreenTest.kt index 7a909ef5f17f..803a2a0b1436 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreenTest.kt @@ -162,7 +162,7 @@ class WelcomeScreenTest { val mockClickListener: () -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { WelcomeScreen( - uiState = WelcomeUiState(), + uiState = WelcomeUiState(showSitePayment = true), uiSideEffect = MutableStateFlow(WelcomeViewModel.UiSideEffect.OpenConnectScreen), onSitePaymentClick = mockClickListener, onRedeemVoucherClick = {}, @@ -295,6 +295,7 @@ class WelcomeScreenTest { val mockPaymentProduct: PaymentProduct = mockk() every { mockPaymentProduct.price } returns ProductPrice("$10") every { mockPaymentProduct.status } returns PaymentStatus.PENDING + val mockShowPendingInfo = mockk<() -> Unit>(relaxed = true) composeTestRule.setContentWithTheme { WelcomeScreen( uiState = @@ -310,7 +311,7 @@ class WelcomeScreenTest { onAccountClick = {}, openConnectScreen = {}, onPurchaseBillingProductClick = { _ -> }, - navigateToVerificationPendingDialog = {}, + navigateToVerificationPendingDialog = mockShowPendingInfo, navigateToDeviceInfoDialog = {} ) } @@ -319,11 +320,7 @@ class WelcomeScreenTest { composeTestRule.onNodeWithTag(PLAY_PAYMENT_INFO_ICON_TEST_TAG).performClick() // Assert - composeTestRule - .onNodeWithText( - "We are currently verifying your purchase, this might take some time. Your time will be added if the verification is successful." - ) - .assertExists() + verify(exactly = 1) { mockShowPendingInfo() } } @Test diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt index 064308bee387..fb2eebff1f78 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -40,6 +41,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.test.TOP_BAR_ACCOUNT_BUTTON +import net.mullvad.mullvadvpn.compose.test.TOP_BAR_SETTINGS_BUTTON import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar @@ -150,7 +153,11 @@ fun MullvadTopBar( }, actions = { if (onAccountClicked != null) { - IconButton(enabled = enabled, onClick = onAccountClicked) { + IconButton( + modifier = Modifier.testTag(TOP_BAR_ACCOUNT_BUTTON), + enabled = enabled, + onClick = onAccountClicked + ) { Icon( painter = painterResource(R.drawable.icon_account), tint = iconTintColor, @@ -160,7 +167,11 @@ fun MullvadTopBar( } if (onSettingsClicked != null) { - IconButton(enabled = enabled, onClick = onSettingsClicked) { + IconButton( + modifier = Modifier.testTag(TOP_BAR_SETTINGS_BUTTON), + enabled = enabled, + onClick = onSettingsClicked + ) { Icon( painter = painterResource(R.drawable.icon_settings), tint = iconTintColor, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt index a950d86218c9..9d5781491edf 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt @@ -120,7 +120,7 @@ fun DnsDialog( } @Composable -private fun DnsDialog( +fun DnsDialog( state: DnsDialogViewState, onDnsInputChange: (String) -> Unit, onSaveDnsClick: () -> Unit, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt index c55d46fcc4b1..39a61238f664 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt @@ -18,7 +18,6 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import com.ramcosta.composedestinations.spec.DestinationStyle -import kotlinx.coroutines.flow.collect import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.PrimaryButton import net.mullvad.mullvadvpn.compose.textfield.MtuTextField @@ -50,12 +49,27 @@ fun MtuDialog(mtuInitial: Int?, navigator: DestinationsNavigator) { } } } + MtuDialog( + mtuInitial = mtuInitial, + onSaveMtu = { mtu -> viewModel.onSaveClick(mtu) }, + onResetMtu = viewModel::onRestoreClick, + onDismiss = navigator::navigateUp + ) +} + +@Composable +fun MtuDialog( + mtuInitial: Int?, + onSaveMtu: (Int) -> Unit, + onResetMtu: () -> Unit, + onDismiss: () -> Unit, +) { val mtu = remember { mutableStateOf(mtuInitial?.toString() ?: "") } val isValidMtu = mtu.value.toIntOrNull()?.isValidMtu() == true AlertDialog( - onDismissRequest = navigator::navigateUp, + onDismissRequest = onDismiss, title = { Text( text = stringResource(id = R.string.wireguard_mtu), @@ -71,7 +85,7 @@ fun MtuDialog(mtuInitial: Int?, navigator: DestinationsNavigator) { onSubmit = { newMtuValue -> val mtuInt = newMtuValue.toIntOrNull() if (mtuInt?.isValidMtu() == true) { - viewModel.onSaveClick(mtuInt) + onSaveMtu(mtuInt) } }, isEnabled = true, @@ -103,7 +117,7 @@ fun MtuDialog(mtuInitial: Int?, navigator: DestinationsNavigator) { onClick = { val mtuInt = mtu.value.toIntOrNull() if (mtuInt?.isValidMtu() == true) { - viewModel.onSaveClick(mtuInt) + onSaveMtu(mtuInt) } } ) @@ -111,13 +125,13 @@ fun MtuDialog(mtuInitial: Int?, navigator: DestinationsNavigator) { PrimaryButton( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.reset_to_default_button), - onClick = viewModel::onRestoreClick + onClick = onResetMtu ) PrimaryButton( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.cancel), - onClick = navigator::navigateUp + onClick = onDismiss ) } }, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt index b2ad37b19557..9b2f495f4d4f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt @@ -57,9 +57,24 @@ data class WireguardCustomPortNavArgs( @Composable fun WireguardCustomPortDialog( navArg: WireguardCustomPortNavArgs, - backNavigator: ResultBackNavigator + backNavigator: ResultBackNavigator, ) { - val port = remember { mutableStateOf(navArg.customPort?.toString() ?: "") } + WireguardCustomPortDialog( + initialPort = navArg.customPort, + allowedPortRanges = navArg.allowedPortRanges, + onSave = { port -> backNavigator.navigateBack(port) }, + onDismiss = backNavigator::navigateBack + ) +} + +@Composable +fun WireguardCustomPortDialog( + initialPort: Int?, + allowedPortRanges: List, + onSave: (Int?) -> Unit, + onDismiss: () -> Unit +) { + val port = remember { mutableStateOf(initialPort?.toString() ?: "") } AlertDialog( title = { @@ -72,23 +87,18 @@ fun WireguardCustomPortDialog( Column(verticalArrangement = Arrangement.spacedBy(Dimens.buttonSpacing)) { PrimaryButton( text = stringResource(id = R.string.custom_port_dialog_submit), - onClick = { backNavigator.navigateBack(port.value.toInt()) }, + onClick = { onSave(port.value.toInt()) }, isEnabled = port.value.isNotEmpty() && - navArg.allowedPortRanges.isPortInValidRanges( - port.value.toIntOrNull() ?: 0 - ) + allowedPortRanges.isPortInValidRanges(port.value.toIntOrNull() ?: 0) ) - if (navArg.customPort != null) { + if (initialPort != null) { NegativeButton( text = stringResource(R.string.custom_port_dialog_remove), - onClick = { backNavigator.navigateBack(null) } + onClick = { onSave(null) } ) } - PrimaryButton( - text = stringResource(id = R.string.cancel), - onClick = backNavigator::navigateBack - ) + PrimaryButton(text = stringResource(id = R.string.cancel), onClick = onDismiss) } }, text = { @@ -98,19 +108,15 @@ fun WireguardCustomPortDialog( onSubmit = { input -> if ( input.isNotEmpty() && - navArg.allowedPortRanges.isPortInValidRanges( - input.toIntOrNull() ?: 0 - ) + allowedPortRanges.isPortInValidRanges(input.toIntOrNull() ?: 0) ) { - backNavigator.navigateBack(result = input.toIntOrNull()) + onSave(input.toIntOrNull()) } }, onValueChanged = { input -> port.value = input }, isValidValue = port.value.isNotEmpty() && - navArg.allowedPortRanges.isPortInValidRanges( - port.value.toIntOrNull() ?: 0 - ), + allowedPortRanges.isPortInValidRanges(port.value.toIntOrNull() ?: 0), maxCharLength = 5, modifier = Modifier.testTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG).fillMaxWidth() ) @@ -119,7 +125,7 @@ fun WireguardCustomPortDialog( text = stringResource( id = R.string.custom_port_dialog_valid_ranges, - navArg.allowedPortRanges.asString() + allowedPortRanges.asString() ), color = MaterialTheme.colorScheme.onBackground.copy(alpha = AlphaDescription), style = MaterialTheme.typography.bodySmall @@ -128,6 +134,6 @@ fun WireguardCustomPortDialog( }, containerColor = MaterialTheme.colorScheme.background, titleContentColor = MaterialTheme.colorScheme.onBackground, - onDismissRequest = backNavigator::navigateBack + onDismissRequest = onDismiss ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt index accc9340f187..a355c2a673d3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt @@ -45,8 +45,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -173,7 +171,6 @@ private fun LoginScreen( onSettingsClick: () -> Unit = {}, ) { ScaffoldWithTopBar( - modifier = Modifier.semantics { testTagsAsResourceId = true }, topBarColor = MaterialTheme.colorScheme.primary, iconTintColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaTopBar), onSettingsClicked = onSettingsClick, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt index ef6e369a6682..6702d393dda5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt @@ -3,7 +3,10 @@ package net.mullvad.mullvadvpn.compose.screen import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.navigation.NavHostController import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.rememberNavHostEngine @@ -19,13 +22,14 @@ import org.koin.androidx.compose.koinViewModel private val changeLogDestinations = listOf(ConnectDestination, OutOfTimeDestination) +@OptIn(ExperimentalComposeUiApi::class) @Composable fun MullvadApp() { val engine = rememberNavHostEngine() val navController: NavHostController = engine.rememberNavController() DestinationsNavHost( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.semantics { testTagsAsResourceId = true }.fillMaxSize(), engine = engine, navController = navController, navGraph = NavGraphs.root diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt index 3a8e897da091..4dde86b9f44f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt @@ -150,7 +150,7 @@ fun OutOfTime( onPurchaseBillingProductClick = { productId -> navigator.navigate(PaymentDestination(productId)) { launchSingleTop = true } }, - navigateToVerifyPendingDialog = { + navigateToVerificationPendingDialog = { navigator.navigate(VerificationPendingDialogDestination) { launchSingleTop = true } } ) @@ -168,7 +168,7 @@ fun OutOfTimeScreen( onSettingsClick: () -> Unit = {}, onAccountClick: () -> Unit = {}, onPurchaseBillingProductClick: (ProductId) -> Unit = { _ -> }, - navigateToVerifyPendingDialog: () -> Unit = {} + navigateToVerificationPendingDialog: () -> Unit = {} ) { val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook() LaunchedEffect(key1 = Unit) { @@ -264,7 +264,7 @@ fun OutOfTimeScreen( onPurchaseBillingProductClick = { productId -> onPurchaseBillingProductClick(productId) }, - onInfoClick = navigateToVerifyPendingDialog, + onInfoClick = navigateToVerificationPendingDialog, modifier = Modifier.padding( start = Dimens.sideMargin, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt index 72f3f516e9bb..efb23cfa3d82 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt @@ -271,26 +271,6 @@ fun VpnSettingsScreen( } } - // when (val dialog = uiState.dialog) { - // is VpnSettingsDialog.CustomPort -> { - // CustomPortDialog( - // customPort = savedCustomPort.value.toDisplayCustomPort(), - // allowedPortRanges = dialog.availablePortRanges, - // onSave = { customPortString -> - // onWireguardPortSelected(Constraint.Only(Port(customPortString.toInt()))) - // }, - // onReset = { - // if (uiState.selectedWireguardPort.isCustom()) { - // onWireguardPortSelected(Constraint.Any()) - // } - // savedCustomPort.value = Constraint.Any() - // onCloseCustomPortDialog() - // }, - // showReset = savedCustomPort.value is Constraint.Only, - // ) - // } - // } - var expandContentBlockersState by rememberSaveable { mutableStateOf(false) } val biggerPadding = 54.dp val topPadding = 6.dp @@ -482,7 +462,8 @@ fun VpnSettingsScreen( Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) InformationComposeCell( title = stringResource(id = R.string.wireguard_port_title), - onInfoClicked = { navigateToWireguardPortInfo(uiState.availablePortRanges) } + onInfoClicked = { navigateToWireguardPortInfo(uiState.availablePortRanges) }, + onCellClicked = { navigateToWireguardPortInfo(uiState.availablePortRanges) }, ) } @@ -556,7 +537,8 @@ fun VpnSettingsScreen( Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) InformationComposeCell( title = stringResource(R.string.quantum_resistant_title), - onInfoClicked = navigateToQuantumResistanceInfo + onInfoClicked = navigateToQuantumResistanceInfo, + onCellClicked = navigateToQuantumResistanceInfo ) } itemWithDivider { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt index 01d434a117d0..ffaa2fff3949 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt @@ -1,5 +1,9 @@ package net.mullvad.mullvadvpn.compose.test +// Top Bar +const val TOP_BAR_ACCOUNT_BUTTON = "top_bar_account_button" +const val TOP_BAR_SETTINGS_BUTTON = "top_bar_settings_button" + // VpnSettingsScreen const val LAZY_LIST_TEST_TAG = "lazy_list_test_tag" const val LAZY_LIST_LAST_ITEM_TEST_TAG = "lazy_list_last_item_test_tag"