Skip to content

Commit

Permalink
Refactor port dialog to use a view model
Browse files Browse the repository at this point in the history
  • Loading branch information
kl committed Sep 17, 2024
1 parent a87b105 commit 00154eb
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@ package net.mullvad.mullvadvpn.compose.dialog

import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithTag
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.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.onNodeWithTagAndText
import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogUiState
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
Expand All @@ -30,17 +40,29 @@ class CustomPortDialogTest {
@SuppressLint("ComposableNaming")
@Composable
private fun testWireguardCustomPortDialog(
initialPort: Port? = null,
portInput: String = "",
isValidInput: Boolean = false,
showResetToDefault: Boolean = false,
allowedPortRanges: List<PortRange> = emptyList(),
onSave: (Port?) -> Unit = { _ -> },
onInputChanged: (String) -> Unit = { _ -> },
onSavePort: (String) -> Unit = { _ -> },
onResetPort: () -> Unit = {},
onDismiss: () -> Unit = {},
) {
val state =
WireguardCustomPortDialogUiState(
portInput = portInput,
isValidInput = isValidInput,
allowedPortRanges = allowedPortRanges,
showResetToDefault = showResetToDefault,
)

WireguardCustomPortDialog(
initialPort = initialPort,
allowedPortRanges = allowedPortRanges,
onSave = onSave,
state,
onInputChanged = onInputChanged,
onSavePort = onSavePort,
onDismiss = onDismiss,
onResetPort = onResetPort,
)
}

Expand All @@ -51,17 +73,112 @@ class CustomPortDialogTest {
// crash the app

// Arrange
setContentWithTheme { testWireguardCustomPortDialog() }
setContentWithTheme {
var input by remember { mutableStateOf("") }
testWireguardCustomPortDialog(portInput = input, onInputChanged = { input = it })
}

// Act
onNodeWithTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG).performTextInput(INVALID_CUSTOM_PORT)
onNodeWithTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG).performTextInput(INVALID_PORT_INPUT)

// Assert
onNodeWithTagAndText(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG, INVALID_CUSTOM_PORT)
onNodeWithTagAndText(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG, INVALID_PORT_INPUT)
.assertDoesNotExist()
}

@Test
fun testEmptyInputResultsInSetPortButtonBeingDisabled() =
composeExtension.use {
// Arrange
setContentWithTheme { testWireguardCustomPortDialog(isValidInput = false) }

// Assert
onNodeWithText("Set port").assertIsNotEnabled()
}

@Test
fun testValidInputResultsInSetPortButtonBeingEnabled() =
composeExtension.use {
// Arrange
setContentWithTheme {
testWireguardCustomPortDialog(portInput = VALID_CUSTOM_PORT, isValidInput = true)
}

// Assert
onNodeWithText("Set port").assertIsEnabled()
onNodeWithText(VALID_CUSTOM_PORT).assertExists()
}

@Test
fun testInvalidInputResultsInSetPortButtonBeingDisabled() =
composeExtension.use {
// Arrange
setContentWithTheme {
testWireguardCustomPortDialog(portInput = INVALID_CUSTOM_PORT, isValidInput = false)
}

// Assert
onNodeWithText("Set port").assertIsNotEnabled()
}

@Test
fun testDialogSubmitOfValidValue() =
composeExtension.use {
// Arrange
val mockedSubmitHandler: (String) -> Unit = mockk(relaxed = true)
setContentWithTheme {
testWireguardCustomPortDialog(
portInput = VALID_CUSTOM_PORT,
isValidInput = true,
onSavePort = mockedSubmitHandler,
)
}

// Act
onNodeWithText("Set port").assertIsEnabled().performClick()

// Assert
verify { mockedSubmitHandler.invoke(VALID_CUSTOM_PORT) }
}

@Test
fun testDialogResetClick() =
composeExtension.use {
// Arrange
val mockedClickHandler: () -> Unit = mockk(relaxed = true)
setContentWithTheme {
testWireguardCustomPortDialog(
portInput = VALID_CUSTOM_PORT,
isValidInput = true,
showResetToDefault = true,
onResetPort = mockedClickHandler,
)
}

// Act
onNodeWithText("Remove custom port").performClick()

// Assert
verify { mockedClickHandler.invoke() }
}

@Test
fun testMtuDialogCancelClick() =
composeExtension.use {
// Arrange
val mockedClickHandler: () -> Unit = mockk(relaxed = true)
setContentWithTheme { testWireguardCustomPortDialog(onDismiss = mockedClickHandler) }

// Assert
onNodeWithText("Cancel").performClick()

// Assert
verify { mockedClickHandler.invoke() }
}

companion object {
const val INVALID_CUSTOM_PORT = "21474836471"
const val INVALID_PORT_INPUT = "21474836471"
const val INVALID_CUSTOM_PORT = "10"
const val VALID_CUSTOM_PORT = "4001"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package net.mullvad.mullvadvpn.compose.dialog
import android.os.Parcelable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.compose.dropUnlessResumed
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.result.EmptyResultBackNavigator
Expand All @@ -18,11 +19,15 @@ import kotlinx.parcelize.Parcelize
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG
import net.mullvad.mullvadvpn.compose.textfield.CustomPortTextField
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.util.asString
import net.mullvad.mullvadvpn.util.inAnyOf
import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogSideEffect
import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogUiState
import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogViewModel
import org.koin.androidx.compose.koinViewModel

@Preview
@Composable
Expand All @@ -47,60 +52,58 @@ data class WireguardCustomPortNavArgs(
@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@Composable
fun WireguardCustomPort(
navArg: WireguardCustomPortNavArgs,
@Suppress("UNUSED_PARAMETER") navArg: WireguardCustomPortNavArgs,
backNavigator: ResultBackNavigator<Port?>,
) {
val viewModel = koinViewModel<WireguardCustomPortDialogViewModel>()

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

CollectSideEffectWithLifecycle(viewModel.uiSideEffect) {
when (it) {
is WireguardCustomPortDialogSideEffect.Success -> backNavigator.navigateBack(it.port)
}
}

WireguardCustomPortDialog(
initialPort = navArg.customPort,
allowedPortRanges = navArg.allowedPortRanges,
onSave = { port -> backNavigator.navigateBack(port) },
onDismiss = backNavigator::navigateBack,
uiState,
onInputChanged = viewModel::onInputChanged,
onSavePort = viewModel::onSaveClick,
onResetPort = viewModel::onResetClick,
onDismiss = dropUnlessResumed { backNavigator.navigateBack() },
)
}

@Composable
fun WireguardCustomPortDialog(
initialPort: Port?,
allowedPortRanges: List<PortRange>,
onSave: (Port?) -> Unit,
state: WireguardCustomPortDialogUiState,
onInputChanged: (String) -> Unit,
onSavePort: (String) -> Unit,
onResetPort: () -> Unit,
onDismiss: () -> Unit,
) {
val port = remember { mutableStateOf(initialPort?.value?.toString() ?: "") }
val isValidPort = port.value.toPortOrNull()?.inAnyOf(allowedPortRanges) ?: false

InputDialog(
title = stringResource(id = R.string.custom_port_dialog_title),
input = {
CustomPortTextField(
value = port.value,
onSubmit = { input ->
if (isValidPort) {
onSave(input.toPortOrNull())
}
},
onValueChanged = { input -> port.value = input },
isValidValue = isValidPort,
value = state.portInput,
onValueChanged = onInputChanged,
onSubmit = onSavePort,
isValidValue = state.isValidInput,
maxCharLength = 5,
modifier = Modifier.testTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG).fillMaxWidth(),
)
},
message =
stringResource(
id = R.string.custom_port_dialog_valid_ranges,
allowedPortRanges.asString(),
state.allowedPortRanges.asString(),
),
confirmButtonEnabled = isValidPort,
confirmButtonEnabled = state.isValidInput,
confirmButtonText = stringResource(id = R.string.custom_port_dialog_submit),
onResetButtonText = stringResource(R.string.custom_port_dialog_remove),
onBack = onDismiss,
onReset =
if (initialPort != null) {
{ onSave(null) }
} else {
null
},
onConfirm = { onSave(port.value.toPortOrNull()) },
onReset = if (state.showResetToDefault) onResetPort else null,
onConfirm = { onSavePort(state.portInput) },
)
}

private fun String.toPortOrNull() = toIntOrNull()?.let { Port(it) }
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import net.mullvad.mullvadvpn.viewmodel.VoucherDialogViewModel
import net.mullvad.mullvadvpn.viewmodel.VpnPermissionViewModel
import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel
import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel
import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogViewModel
import org.apache.commons.validator.routines.InetAddressValidator
import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
Expand Down Expand Up @@ -182,6 +183,7 @@ val uiModule = module {
viewModel { DeviceRevokedViewModel(get(), get()) }
viewModel { MtuDialogViewModel(get(), get()) }
viewModel { DnsDialogViewModel(get(), get(), get()) }
viewModel { WireguardCustomPortDialogViewModel(get()) }
viewModel { LoginViewModel(get(), get(), get()) }
viewModel { PrivacyDisclaimerViewModel(get(), IS_PLAY_BUILD) }
viewModel {
Expand Down
Loading

0 comments on commit 00154eb

Please sign in to comment.