Skip to content

Commit

Permalink
Add view model and ui test
Browse files Browse the repository at this point in the history
  • Loading branch information
sabercodic committed Nov 22, 2023
1 parent da2c1b9 commit 58ca726
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package net.mullvad.mullvadvpn.compose.screen

import androidx.compose.runtime.remember
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule
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.mockk
import io.mockk.mockkObject
import io.mockk.verify
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.compose.test.VOUCHER_INPUT_TEST_TAG
import net.mullvad.mullvadvpn.util.VoucherRegexHelper
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class RedeemVoucherDialogTest {
@get:Rule val composeTestRule = createComposeRule()

private var uiState = VoucherDialogUiState.INITIAL

@Before
fun setup() {
mockkObject(VoucherRegexHelper)
}

@Test
fun testDismissDialog() {
// Arrange
val mockedClickHandler: (Boolean) -> Unit = mockk(relaxed = true)
composeTestRule.setContentWithTheme {
RedeemVoucherDialogScreen(
uiState = uiState,
onVoucherInputChange = {},
onRedeem = {},
onDismiss = mockedClickHandler
)
}

// Act

composeTestRule.onNodeWithText(CANCEL_BUTTON_TEXT).performClick()

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

@Test
fun testInsertValidVoucher() {
// Arrange
composeTestRule.setContentWithTheme {
var voucherInput = remember { "" }
RedeemVoucherDialogScreen(
uiState = uiState.copy(voucherInput = voucherInput),
onVoucherInputChange = { voucherInput = it },
onRedeem = {},
onDismiss = {}
)
}

// Act
composeTestRule.onNodeWithTag(VOUCHER_INPUT_TEST_TAG).performTextInput(DUMMY_VALID_VOUCHER)

// Assert
composeTestRule
.onNodeWithTag(VOUCHER_INPUT_TEST_TAG)
.assert(hasText(DUMMY_VALID_VOUCHER_FORMATED))
}

@Test
fun testInsertInvalidVoucher() {
// Arrange
composeTestRule.setContentWithTheme {
var voucherInput = remember { "" }
RedeemVoucherDialogScreen(
uiState = uiState.copy(voucherInput = voucherInput),
onVoucherInputChange = { voucherInput = it },
onRedeem = {},
onDismiss = {}
)
}

// Act
composeTestRule
.onNodeWithTag(VOUCHER_INPUT_TEST_TAG)
.performTextInput(DUMMY_INVALID_VOUCHER)

// Assert
composeTestRule.onNodeWithText(REDEEM_BUTTON_TEXT).assert(isNotEnabled())
}

companion object {
private const val REDEEM_BUTTON_TEXT = "Redeem"
private const val CANCEL_BUTTON_TEXT = "Cancel"
private const val DUMMY_VALID_VOUCHER = "DUMMYVALIDVOUCHE"
private const val DUMMY_VALID_VOUCHER_FORMATED = "DUMM-YVAL-IDVO-UCHE"
private const val DUMMY_INVALID_VOUCHER = "DUMM-YINV-ALID-VOUC-HER"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
Expand All @@ -29,6 +34,7 @@ import net.mullvad.mullvadvpn.compose.button.VariantButton
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorSmall
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.compose.test.VOUCHER_INPUT_TEST_TAG
import net.mullvad.mullvadvpn.compose.textfield.CustomTextField
import net.mullvad.mullvadvpn.compose.util.MAX_VOUCHER_LENGTH
import net.mullvad.mullvadvpn.compose.util.vouchersVisualTransformation
Expand Down Expand Up @@ -217,20 +223,25 @@ private fun EnterVoucherBody(
onVoucherInputChange: (String) -> Unit = {},
onRedeem: (voucherCode: String) -> Unit
) {
var voucherInput by remember { mutableStateOf("") }
CustomTextField(
value = uiState.voucherInput,
value = voucherInput,
onSubmit = { input ->
if (uiState.voucherInput.length == VOUCHER_LENGTH) {
onRedeem(input)
}
},
onValueChanged = { input -> onVoucherInputChange(input) },
onValueChanged = { input ->
voucherInput = input
onVoucherInputChange(input)
},
isValidValue =
uiState.voucherInput.isEmpty() || uiState.voucherInput.length == MAX_VOUCHER_LENGTH,
keyboardType = KeyboardType.Password,
placeholderText = stringResource(id = R.string.voucher_hint),
visualTransformation = vouchersVisualTransformation(),
isDigitsOnlyAllowed = false
isDigitsOnlyAllowed = false,
modifier = Modifier.testTag(VOUCHER_INPUT_TEST_TAG)
)
Spacer(modifier = Modifier.height(Dimens.smallPadding))
Row(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ const val PLAY_PAYMENT_INFO_ICON_TEST_TAG = "play_payment_info_icon_test_tag"

const val LOGIN_TITLE_TEST_TAG = "login_title_test_tag"
const val LOGIN_INPUT_TEST_TAG = "login_input_test_tag"

// VoucherDialog
const val VOUCHER_INPUT_TEST_TAG = "voucher_input_test_tag"
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.state.LoginUiState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.constant.VOUCHER_LENGTH
Expand All @@ -33,7 +32,7 @@ class VoucherDialogViewModel(
) : ViewModel() {

private val vmState = MutableStateFlow<VoucherDialogState>(VoucherDialogState.Default)
private val voucherInput = MutableStateFlow(LoginUiState.INITIAL.accountNumberInput)
private val voucherInput = MutableStateFlow(VoucherDialogUiState.INITIAL.voucherInput)

private val _shared: SharedFlow<ServiceConnectionContainer> =
serviceConnectionManager.connectionState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package net.mullvad.mullvadvpn.utils

import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.util.VoucherRegexHelper
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

private const val IS_ACCEPTED_FORMAT = true
private const val IS_UNACCEPTED_FORMAT = false

@RunWith(Parameterized::class)
class VoucherRegexHelperParameterizedTest(
private val isValid: Boolean,
private val voucher: String
) {
@get:Rule val testCoroutineRule = TestCoroutineRule()

companion object {
@JvmStatic
@Parameterized.Parameters
fun data(): Collection<Array<Any>> =
listOf(
arrayOf(IS_ACCEPTED_FORMAT, acceptable_inputs_for_voucher[0]),
arrayOf(IS_ACCEPTED_FORMAT, acceptable_inputs_for_voucher[1]),
arrayOf(IS_ACCEPTED_FORMAT, acceptable_inputs_for_voucher[2]),
arrayOf(IS_ACCEPTED_FORMAT, acceptable_inputs_for_voucher[3]),
arrayOf(IS_ACCEPTED_FORMAT, acceptable_inputs_for_voucher[4]),
arrayOf(IS_ACCEPTED_FORMAT, acceptable_inputs_for_voucher[5]),
arrayOf(IS_ACCEPTED_FORMAT, acceptable_inputs_for_voucher[6]),
arrayOf(IS_ACCEPTED_FORMAT, acceptable_inputs_for_voucher[7]),
arrayOf(IS_UNACCEPTED_FORMAT, non_acceptable_inputs_for_voucher[0]),
arrayOf(IS_UNACCEPTED_FORMAT, non_acceptable_inputs_for_voucher[1]),
arrayOf(IS_UNACCEPTED_FORMAT, non_acceptable_inputs_for_voucher[2])
)

private val acceptable_inputs_for_voucher =
arrayOf(
"1",
"a",
"A",
"AAAA",
"AAAABBBB11112222",
"AAAA BBBB 1111 2222",
"AAAA-AAAA-1111-2222\r",
"AAAA-AAAA-1111-2222\n",
)
private val non_acceptable_inputs_for_voucher =
arrayOf(
"@",
"AAAABBBBCCCCDDDD\t",
"AAAA_BBBB_CCCC_DDDD",
)
}

@Test
fun testVoucherFormat() {
assertThat(VoucherRegexHelper.validate(voucher), equalTo(isValid))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,37 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlin.test.assertIs
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.model.VoucherSubmission
import net.mullvad.mullvadvpn.model.VoucherSubmissionError
import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
import net.mullvad.mullvadvpn.ui.serviceconnection.VoucherRedeemer
import net.mullvad.mullvadvpn.ui.serviceconnection.voucherRedeemer
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test

class VoucherDialogViewModelTest {
@get:Rule val testCoroutineRule = TestCoroutineRule()

private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
private val mockVoucherRedeemer: VoucherRedeemer = mockk()
private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
private val mockVoucherSubmission: VoucherSubmission = mockk()
private val mockConnectionProxy: ConnectionProxy = mockk()
private val serviceConnectionState =
MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)

private val mockVoucherRedeemer: VoucherRedeemer = mockk()
private val mockResources: Resources = mockk()

private val mockVoucherSubmissionErrorResult: VoucherSubmissionResult =
Expand All @@ -36,9 +48,9 @@ class VoucherDialogViewModelTest {
@Before
fun setUp() {
mockkStatic(CACHE_EXTENSION_CLASS)
every { mockServiceConnectionManager.connectionState.value.readyContainer() } returns
mockServiceConnectionContainer
every { mockServiceConnectionManager.connectionState } returns serviceConnectionState
every { mockServiceConnectionContainer.voucherRedeemer } returns mockVoucherRedeemer
every { mockServiceConnectionContainer.connectionProxy } returns mockConnectionProxy

viewModel =
VoucherDialogViewModel(
Expand All @@ -53,21 +65,48 @@ class VoucherDialogViewModelTest {
}

@Test
@Ignore("TODO: Fix this failing test and then enable it again.")
fun test_submit_invalid_voucher() = runTest {
val voucher = DUMMY_VALID_VOUCHER
fun testSubmitVoucher() = runTest {
val voucher = DUMMY_INVALID_VOUCHER
val dummyStringResource = DUMMY_STRING_RESOURCE

// Arrange
serviceConnectionState.value =
ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
every { mockServiceConnectionManager.voucherRedeemer() } returns mockVoucherRedeemer
every { mockResources.getString(any()) } returns dummyStringResource
coEvery { mockVoucherRedeemer.submit(voucher) } returns mockVoucherSubmissionErrorResult
// Act, Assert
// Act
assertIs<VoucherDialogState.Default>(viewModel.uiState.value.voucherViewModelState)
viewModel.onRedeem(voucher)

// Assert
coVerify(exactly = 1) { mockVoucherRedeemer.submit(voucher) }
}

@Test
fun testInsertValidVoucher() = runTest {
val voucher = DUMMY_VALID_VOUCHER
val dummyStringResource = DUMMY_STRING_RESOURCE

// Arrange
val uiStates = viewModel.uiState
every { mockServiceConnectionManager.voucherRedeemer() } returns mockVoucherRedeemer
every { mockResources.getString(any()) } returns dummyStringResource
every { mockVoucherSubmission.timeAdded } returns 0
coEvery { mockVoucherRedeemer.submit(voucher) } returns
VoucherSubmissionResult.Ok(mockVoucherSubmission)

// Act
viewModel.onRedeem(voucher)

// Assert
Assert.assertTrue(uiStates.value.voucherViewModelState is VoucherDialogState.Default)
}

companion object {
private const val CACHE_EXTENSION_CLASS = "net.mullvad.mullvadvpn.util.CacheExtensionsKt"
private const val DUMMY_VALID_VOUCHER = "DUMMY_VALID_VOUCHER"
private const val DUMMY_STRING_RESOURCE = "DUMMY_STRING_RESOURCE"
private const val DUMMY_VALID_VOUCHER = "dummy_valid_voucher"
private const val DUMMY_INVALID_VOUCHER = "dummy_invalid_voucher"
private const val DUMMY_STRING_RESOURCE = "dummy_string_resource"
}
}

0 comments on commit 58ca726

Please sign in to comment.