From c65bd7147e8f2000a8f9df390ba630d909153e74 Mon Sep 17 00:00:00 2001 From: saber safavi Date: Fri, 17 Nov 2023 13:52:55 +0100 Subject: [PATCH] Add view model and ui test --- .../compose/screen/RedeemVoucherDialogTest.kt | 149 ++++++++++++++++++ .../compose/dialog/RedeemVoucherDialog.kt | 5 +- .../compose/test/ComposeTestTagConstants.kt | 3 + .../viewmodel/VoucherDialogViewModel.kt | 3 +- .../VoucherRegexHelperParameterizedTest.kt | 45 ++++++ .../viewmodel/VoucherDialogViewModelTest.kt | 68 ++++++-- 6 files changed, 256 insertions(+), 17 deletions(-) create mode 100644 android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt create mode 100644 android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/VoucherRegexHelperParameterizedTest.kt diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt new file mode 100644 index 000000000000..c07cb1aa6b32 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt @@ -0,0 +1,149 @@ +package net.mullvad.mullvadvpn.compose.screen + +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.VoucherDialogState +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() + + @Before + fun setup() { + mockkObject(VoucherRegexHelper) + } + + @Test + fun testDismissDialog() { + // Arrange + val mockedClickHandler: (Boolean) -> Unit = mockk(relaxed = true) + composeTestRule.setContentWithTheme { + RedeemVoucherDialogScreen( + uiState = VoucherDialogUiState.INITIAL, + onVoucherInputChange = {}, + onRedeem = {}, + onDismiss = mockedClickHandler + ) + } + + // Act + composeTestRule.onNodeWithText(CANCEL_BUTTON_TEXT).performClick() + + // Assert + verify { mockedClickHandler.invoke(false) } + } + + @Test + fun testDismissDialogAfterSuccessfulRedeem() { + // Arrange + val mockedClickHandler: (Boolean) -> Unit = mockk(relaxed = true) + composeTestRule.setContentWithTheme { + RedeemVoucherDialogScreen( + uiState = + VoucherDialogUiState(voucherViewModelState = VoucherDialogState.Success(0)), + onVoucherInputChange = {}, + onRedeem = {}, + onDismiss = mockedClickHandler + ) + } + + // Act + composeTestRule.onNodeWithText(GOT_IT_BUTTON_TEXT).performClick() + + // Assert + verify { mockedClickHandler.invoke(true) } + } + + @Test + fun testInsertVoucher() { + // Arrange + val mockedClickHandler: (String) -> Unit = mockk(relaxed = true) + composeTestRule.setContentWithTheme { + RedeemVoucherDialogScreen( + uiState = VoucherDialogUiState(), + onVoucherInputChange = mockedClickHandler, + onRedeem = {}, + onDismiss = {} + ) + } + + // Act + composeTestRule.onNodeWithTag(VOUCHER_INPUT_TEST_TAG).performTextInput(DUMMY_VOUCHER) + + // Assert + verify { mockedClickHandler.invoke(DUMMY_VOUCHER) } + } + + @Test + fun testVerifyingState() { + // Arrange + composeTestRule.setContentWithTheme { + RedeemVoucherDialogScreen( + uiState = + VoucherDialogUiState(voucherViewModelState = VoucherDialogState.Verifying), + onVoucherInputChange = {}, + onRedeem = {}, + onDismiss = {} + ) + } + + // Assert + composeTestRule.onNodeWithText("Verifying voucher…").assertExists() + } + + @Test + fun testSuccessState() { + // Arrange + composeTestRule.setContentWithTheme { + RedeemVoucherDialogScreen( + uiState = + VoucherDialogUiState(voucherViewModelState = VoucherDialogState.Success(0)), + onVoucherInputChange = {}, + onRedeem = {}, + onDismiss = {} + ) + } + + // Assert + composeTestRule.onNodeWithText("Voucher was successfully redeemed.").assertExists() + } + + @Test + fun testErrorState() { + // Arrange + composeTestRule.setContentWithTheme { + RedeemVoucherDialogScreen( + uiState = + VoucherDialogUiState( + voucherViewModelState = VoucherDialogState.Error(ERROR_MESSAGE) + ), + onVoucherInputChange = {}, + onRedeem = {}, + onDismiss = {} + ) + } + + // Assert + composeTestRule.onNodeWithText(ERROR_MESSAGE).assertExists() + } + + companion object { + private const val REDEEM_BUTTON_TEXT = "Redeem" + private const val CANCEL_BUTTON_TEXT = "Cancel" + private const val GOT_IT_BUTTON_TEXT = "Got it!" + private const val DUMMY_VOUCHER = "DUMMY____VOUCHER" + private const val ERROR_MESSAGE = "error_message" + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt index 29d4807b42e0..1c48a8a64aa8 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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 @@ -29,6 +30,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 @@ -230,7 +232,8 @@ private fun EnterVoucherBody( 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( 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..e3cb4faa5b32 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 @@ -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" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt index 44a07edc2b94..4b177d58fa58 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt @@ -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 @@ -33,7 +32,7 @@ class VoucherDialogViewModel( ) : ViewModel() { private val vmState = MutableStateFlow(VoucherDialogState.Default) - private val voucherInput = MutableStateFlow(LoginUiState.INITIAL.accountNumberInput) + private val voucherInput = MutableStateFlow("") private val _shared: SharedFlow = serviceConnectionManager.connectionState diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/VoucherRegexHelperParameterizedTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/VoucherRegexHelperParameterizedTest.kt new file mode 100644 index 000000000000..6ba41c0d11b6 --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/VoucherRegexHelperParameterizedTest.kt @@ -0,0 +1,45 @@ +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> = + listOf( + arrayOf(IS_ACCEPTED_FORMAT, "1"), + arrayOf(IS_ACCEPTED_FORMAT, "a"), + arrayOf(IS_ACCEPTED_FORMAT, "A"), + arrayOf(IS_ACCEPTED_FORMAT, "AAAA"), + arrayOf(IS_ACCEPTED_FORMAT, "AAAABBBB11112222"), + arrayOf(IS_ACCEPTED_FORMAT, "AAAA BBBB 1111 2222"), + arrayOf(IS_ACCEPTED_FORMAT, "AAAA-AAAA-1111-2222\r"), + arrayOf(IS_ACCEPTED_FORMAT, "AAAA-AAAA-1111-2222\n"), + arrayOf(IS_UNACCEPTED_FORMAT, "@"), + arrayOf(IS_UNACCEPTED_FORMAT, "AAAABBBBCCCCDDDD\t"), + arrayOf(IS_UNACCEPTED_FORMAT, "AAAA_BBBB_CCCC_DDDD") + ) + } + + @Test + fun testVoucherFormat() { + assertThat(VoucherRegexHelper.validate(voucher), equalTo(isValid)) + } +} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt index bf87799d876d..2aeae3741e3d 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt @@ -7,16 +7,23 @@ 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 @@ -24,21 +31,25 @@ class VoucherDialogViewModelTest { @get:Rule val testCoroutineRule = TestCoroutineRule() private val mockServiceConnectionManager: ServiceConnectionManager = mockk() - private val mockVoucherRedeemer: VoucherRedeemer = mockk() private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk() - private val mockResources: Resources = mockk() + private val mockVoucherSubmission: VoucherSubmission = mockk() + private val mockConnectionProxy: ConnectionProxy = mockk() + private val serviceConnectionState = + MutableStateFlow(ServiceConnectionState.Disconnected) - private val mockVoucherSubmissionErrorResult: VoucherSubmissionResult = - VoucherSubmissionResult.Error(VoucherSubmissionError.OtherError) + private val mockVoucherRedeemer: VoucherRedeemer = mockk() + private val mockResources: Resources = mockk() private lateinit var viewModel: VoucherDialogViewModel @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 + + serviceConnectionState.value viewModel = VoucherDialogViewModel( @@ -53,21 +64,50 @@ 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 + every { mockVoucherSubmission.timeAdded } returns 0 + coEvery { mockVoucherRedeemer.submit(voucher) } returns + VoucherSubmissionResult.Ok(mockVoucherSubmission) + // Act + assertIs(viewModel.uiState.value.voucherViewModelState) viewModel.onRedeem(voucher) + + // Assert coVerify(exactly = 1) { mockVoucherRedeemer.submit(voucher) } } + @Test + fun testInsertInvalidVoucher() = runTest { + val voucher = DUMMY_INVALID_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.Error(VoucherSubmissionError.OtherError) + + // 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" } }